這篇文章首發於 App Coda https://www.appcoda.com.tw/ios-app-%e7%92%b0%e5%a2%83%e7%ae%a1%e7%90%86/


真正產品化的 App,通常都有不同的環境與配置,包括環境資訊與後端不同環境的位置,上線後測試功能分流,一般實驗性可以很容易地上手 CI/CD,但在實戰中可能需要更加複雜且靈活的設置,讓我們來看看,這些配置在實戰中是什麼樣子,原始碼都在這裡

在專案準備中,我們使用 GitLab 來實作,透過 GitLab CI/CD 完成我們產品 App 中的 CI/CD Pipeline 流水線的建置,然後建立一個新的 App 當作我們產品的標的。並且透過分出各個環境來代表實戰中的項目環境。同時,配置 fastlane 在我們將程式碼推送到 GitLab 執行,他會幫我們在不同環境中執行各個事項,比如建置、打包,根據環境推送到不同的 App Store 項目中等等。後面還會提供更進階的配置,比如 firebase 要怎麼納入我們的環境管理中。好,讓我們開始吧!

專案準備

在這個步驟中我們有兩個目標,建立好 GitLab 與 GitLab Runner,還有新增一個 XCode 專案,當作我們的產品標的。

建立 GitLab專案與 Runner

這個步驟的目標是讓我們建立一個 GitLab 專案,並且設置好 Runner(執行 CI 的機器)

首先讓我們先再,GitLab建立一個新專案。

然後讓我們先設定好,GitLab-CI Runner,這邊就不再贅述,可以參考這篇

GitLab 初學者指南: 輕鬆為 iOS 專案設定持續集成(CI/CD)

接下來我們需要一個新的 iOS 專案,當作我們產品App 的標的!

建立新的 iOS 專案

這個步驟的目標是建立一個專案當作範例。

接下來讓我們開啟一個新專案

預覽一下我們的新專案

接下來讓我們模擬一個產品中,有很多種環境!

配置環境

這個步驟中目標,首先我們會先定義環境,然後透過環境定義抽換各種變數,

接下來讓我們加入以下環境

  1. Debug
  2. Staging
  3. Release

這邊有個小伏筆,Staging 打成 Sta’t’ing,後面會介紹如何更正。

讓我們按下 New Scheme

填入我們的 Scheme 名稱

接下來讓我們新增 Config

建立 Config 資料夾

新增Config檔案

我們一樣需要三個檔案

  1. Debug.xcconfig
  2. Staging.xcconfig
  3. Release.xcconfig

綁定 Config

最後應該像這樣

填入與設置變數

這邊我們模擬不同環境有不同的 API 位置,來作配置

Debug.xcconfig

API_URL = https:\/\/yasuoyuhao.klearthink.com/debug/

Staging.xcconfig

API_URL = https:\/\/yasuoyuhao.klearthink.com/staging/

Release.xcconfig

API_URL = https:\/\/yasuoyuhao.klearthink.com/release/

預覽一下

我們用網址來代表不同環境的後端 API 位置

修改Info.plist

<key>API_URL</key>
	<string>$(API_URL)</string>

建立 ServicesURL

import Foundation

public struct ServicesURL {
    
    static var baseurl: String {
        return (Bundle.main.infoDictionary?["API_URL"] as? String)?.replacingOccurrences(of: "\\", with: "") ?? ""
    }
}

預覽一下

然後讓我們的畫面讀取這個數值

最後讓我們綁定 Scheme 並且看看效果

Debug 模式下

Staging 模式下

Release 模式下

太棒了,我們已經成功的切換環境變數配置

最後先讓我們的成果,推上剛建立好的 GitLab

推到遠端

確認是否真的推上了

接下來我們需要配置好 fastlane 與 Gitlab CI,讓我們的建置、配置、測試、部署完全自動化!

配置-fastlane、gitlab-ci

在這個步驟中我們會建置 fastlane、gitlab-ci,達成我們自動化建置、測試、部署的目標。

初始化 fastlane

配置 fastlane

Fastfile

# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
#     https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
#     https://docs.fastlane.tools/plugins/available-plugins
#

# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane

default_platform(:ios)

platform :ios do

  def install_pods
    cocoapods(
      clean: true,
      podfile: "Podfile",
      try_repo_update_on_error: true
    )
  end
  
  desc "Push a new beta build to TestFlight"
  lane :beta do
    sigh(force: true)
    install_pods()
    build_number = number_of_commits()
    increment_build_number(
      build_number: build_number # set a specific number
    )
    build_app(workspace: "product-app-env-demo.xcworkspace", 
      scheme: "Staging",
      configuration: "Staging")
    # upload_to_testflight
    pilot(skip_waiting_for_build_processing: true)
  end

  desc "Publish a new build to prod"
  lane :prod do
    sigh(force: true)
    install_pods()
    build_number = number_of_commits()
    increment_build_number(
      build_number: build_number # set a specific number
    )
    build_app(workspace: "product-app-env-demo.xcworkspace", scheme: "Release", configuration: "Release")
    # upload_to_testflight
    pilot(skip_waiting_for_build_processing: true)
  end

  desc "Runs all the tests"
  lane :tests do
    install_pods()
    scan(workspace: "product-app-env-demo.xcworkspace",
        scheme: "product-app-env-demo",
        devices: ["iPhone Xs"],
        code_coverage: true,
        clean: true)
  end
end

Appfile

這邊要記得換成自己的資訊

app_identifier("me.yasuoyuhao.product-app-env-demo") # The bundle identifier of your app
apple_id("[email protected]") # Your Apple email address

itc_team_id("119027236") # App Store Connect Team ID
team_id("Q7DP9UNF3W") # Developer Portal Team ID

# For more information about the Appfile, see:
#     https://docs.fastlane.tools/advanced/#appfile

初始化 cocoapods

編輯 Podfile,並且安裝

重新修改 Xcode 配置

新增檔案 .gitlab-ci.yml

填入以下內容:

Stages 分為三個項目

  1. unit_tests 代表跑測試
  2. develop 代表 Staging 環境部署,並且 Release to Testfight
  3. master 代表 Release 環境部署,並且 Release to Testfight
stages:
  - unit_tests
  - develop
  - master

variables:
  LANG: "en_US.UTF-8"
  LC_ALL: "en_US.UTF-8"
  GIT_SUBMODULE_STRATEGY: normal
 
# before_script:
#   - sudo gem install bundler && bundle update
#   - sudo gem install cocoapods
 
unit_tests:
  tags:
    - ios
  stage: unit_tests
  script:
    - bundle exec fastlane tests
  only:
    - branches
  except:
    - tags
  artifacts:
    paths:
      - fastlane/test_output/
  allow_failure: false

develop:
  tags:
    - ios
  stage: develop
  script:
    - bundle exec fastlane beta
  only:
    - /^release*/
    - release
    - /^develop-.*/
    - develop
  environment:
    name: develop

master:
  tags:
    - ios
  stage: master
  script:
    - bundle exec fastlane prod
  only:
    - /^release*/
    - release
    - master
  environment:
    name: production

好,接下來讓我們把這個成果展示在 Testflight 中吧!所以我們需要先配置一個 AppStore 項目

配置-AppStore

在這個步驟中我們會建立 AppStore 的項目,讓我們在 Gitlab 打包好的項目可以上傳到 Testflight 中。

接下來讓我們到 AppStore 新增一個 App

新增完成

這邊應該大家都有經驗,App開起來

接下來讓我們驗證一下我們剛剛撰寫的 fastlane 是否正常

測試-fastlane

我們做了很多努力!現在是該來測試一下我們的執行成果了,我們先來驗證 fastlane 是否正常!

接下來讓我在本地測試一下 fastlane 指令(如果遇到憑證問題,可以先到 Xcode 裡面設定好)

記得在Gemfile 加入 cocoapods

接下來讓我們看看,如果在過程中,環境資訊配置錯誤了,該怎麼處理?

情境-環境配置錯了怎麼改

這邊我們可以模擬一個錯誤情境,我們的環境配置資訊錯誤了,應該怎麼修改?

接下來的情境是,如果環境輸入錯誤,要如何更換配置呢

讓我們直接建立一個 Config Staging.xcconfig

直接更換配置就可以了

另外記得 Scheme 也要更換

然後確認看看

最後讓我們再跑一次 fastlane beta

應該就成功了!

大功告成!我們再也不怕環境配置輸入錯誤而措手不急了!接下來讓我們推送到 Gitlab,測試與查看 Gitlab CI 的效果吧!

測試-gitlab-ci

我們的 Gitlab CI 設置會幫我們部署到Testflight 上,我們就來試試看吧!

終於到這裡了!

再來讓我們推上 GitLab,跑跑看 CI 吧!

推送發現 CI 執行中

等待一下之後發現完成了!

太棒了,我們的 GItlab CI 執行成功了,接下來我們需要到 Testfight 上查看,我們部署出去的 App 是不是真的可以使用 Testfight 提供測試。

檢驗成果-Testfight

現在是收割的時刻了!我們來查看 Testflight 吧!

完成 Deploy 了!

那我們來檢查一下 Testflight 上有沒有吧

我們完成了!!

目前我們把實戰中最基礎的配置部分都完成了!接下來讓我們看看,要如何透過不同的分支打包與部署出不同環境的 App

進階-不同分支的App Release

我們會使用分支來打包與部署不同環境的 App

接下來示範

  1. develop 分支 -> 推送 Staging 環境
  2. master 分支 -> 推送 Release 環境

可以看到我們的 CI 檔案中已經設置好應有的分支,再來只要推送就好了!

哇!我們成功地讓不同分支可以讀到不同的環境配置了!接下來讓我們再更近一步,將不同環境的App 部署到不同的 App Store 項目中,使得我們可以在正式與測試環境中很自由的使用與測試。

進階-把環境分到不同的 App 中

我們將不同環境配置的項目,打包到不同的 App Store 項目中吧!

讓我們在建立一個AppStore App

然後再 Config 檔案中加入變數

這邊有一個要更注意的點,也是常常會出現的坑!

記得project.pbxproj內也要改

最後修改一下 Appfile

測試一下,可以發現替換成功

回到 AppStore 查看,確定Deploy成功

確認 Develop 分支下的包ID

確認 Master 分支下的包ID

快樂的 Testflight

哇,我們現在環境配置與建置已經非常完整了,App 中常常會加入 Firebase 來使用,是很棒的工具庫,那我要如何將Firebase也換環境呢?接下來就讓我們來達成他!

進階-Firebase 配置也可以分環境?

我們將進行 Firebase 配置檔案也可以分環境!

我們知道 firebase 都有一個設定檔案GoogleService-Info.plist

如果我想依照環境設置不同的設定檔案,該如何做?

設置 Firebase

加入 iOS 應用程式

輸入基本資訊 Release 用

安裝 Pod

初始化 SDK

在建立一個 Staging 用

然後將下載下來的檔案,分別命名成

  1. GoogleService-Info-Debug.plist
  2. GoogleService-Info-Staging.plist
  3. GoogleService-Info-Release.plist

最後來加一個腳本

腳本內容如下

# Type a script or drag a script file from your workspace to insert its path.
RESOURCE_PATH=${SRCROOT}/${PRODUCT_NAME}/GoogleServiceInfoPlists/GoogleService-Info-$CONFIGURATION.plist

BUILD_APP_DIR=${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app

echo "Copying ${RESOURCE_PATH} to ${BUILD_APP_DIR}"
cp "${RESOURCE_PATH}" "${BUILD_APP_DIR}/GoogleService-Info.plist"

如此一來,就會在編譯期間抓取需要的設定檔案,實現分離環境讀取 firebase 設定!

太棒了,連 firebase 都可以分環境了!太棒了,我們幾乎將所有實戰中遇到的問題都配置完成了!接下來讓我們看如何更完整,把 Slack 通知也一併的配置進來。

進階-Slack 通知

我們要目標是讓 ChatOps 活起來,如此一來團隊中每個成員都可以很好的知道目前 App 的動向。

最後讓我們加碼一下,把 slack 加入吧!

效果如下:

很簡單,我們需要修改 Fastfile,加入 slack插件,將slack url 替換成自己設置好的 webhook!就可以了

def post_slack_message(message)
  slack(
    message: "App successfully uploaded to iTunesConnect. #{message}",
    success: true,
    slack_url: "https://hooks.slack.com/services/xxxx/xxx/xxx"
  )
end

最後再加入我們的 Deploy中

desc "Publish a new build to prod"
  lane :prod do
    sigh(force: true)
    install_pods()
    build_number = number_of_commits()
    increment_build_number(
      build_number: build_number # set a specific number
    )
    build_app(workspace: "product-app-env-demo.xcworkspace", scheme: "Release", configuration: "Release")
    # upload_to_testflight
    pilot(skip_waiting_for_build_processing: true)
    post_slack_message("production version #{build_number}")
  end

最後我們來看一下訊息!

總結

大功告成!到目前為止我們就完成了真正一個產品開發的環境管理,因為有變數的關係,理論上是所有服務都可以分環境的,FB SDK、Line SDK 等等,授權與認證完全可以用變數的方式取代,CI/CD也大大簡化了人工分環境的困擾與繁瑣的流程。也成功地引入 DevOps、 Code as Infrastructure 的精神,我是 yasuoyuhao,希望你喜歡我的文章,如果你有不同的想法或問題,歡迎寄信聯絡我 [email protected],祝你有個美好 Coding 夜晚,感謝你的閱讀,我們下次見。別忘了原始碼都在這裡

澄思設計-沈思世界的解決方案
解決問題的路上,也給大家解決過的問題不同的角度思考方案,包含軟體工程、架構、使用者體驗、專案管理等方法論。澄思設計以顧問的角色,積極解決客戶的問題。理解客戶的想法,這個客戶不一定是企業,也可能是個人,解決企業問題,我們使用專案解決,解決個人問題,我們使用產品解決。我們想要找的人,是能夠解決這個世界上各種大大小小問題的人,無論是透過溝通、技術解決,同時他應該會有積極與強大的自學能力,對於解決問題充滿熱誠,與團隊、與公司共同搭上火箭成長,那你可能就是我們要找的人Klearthink Design Co., Ltd.
如果你對我們公司有興趣,歡迎參考我們的職缺,我們有機會聊聊吧 :D
職缺參考

yasuoyuhao,自認為終身學習者,對多領域都有濃厚興趣,喜歡探討各種事物。目前專職軟體開發,系統架構設計,企業解決方案。最喜歡 iOS with Swift, [email protected]
如果喜歡我的文章,可以按下喜歡或追隨讓我知道呦,拍手可以拍 50 下,更歡迎許多大神指點討論。感謝您的閱讀。
部落格:yasuoyuhao’s Area