Swift 程式語言

Playground 驅動開發 :助你加快編譯過程 大大提高開發效率

Playground 驅動開發 :助你加快編譯過程 大大提高開發效率
Playground 驅動開發 :助你加快編譯過程 大大提高開發效率
In: Swift 程式語言, Xcode
本篇原文 (標題: Playground driven development in Swift) 刊登於作者 Medium,由 Khoa Pham 所著,並授權翻譯及轉載。

需要快速調整 UI 的需求

流動程式開發者的使命,就是為用戶提供最佳用戶體驗,透過應用程式讓他們的生活更愉快、更輕鬆。其中一項任務就是確保 UI(用戶操作介面)好看而正確。大多數時候,我們可以說應用程式是將數據美化呈現,我們主要從後端獲取 JSON 數據,將其解析為模型,然後使用 UIView(主要是 UITableView 或 UICollectionView)進行渲染。

進行 iOS 開發時,開發人員需要根據設計不斷調整 UI,以切合小型手持設備。這個過程牽涉更改程式碼、編譯、等待、檢查,然後再次更改程式碼等…… 像 Flawless App 這樣的工具有助我們輕鬆比較 iOS app 和 Sketch 設計的結果。但真正的痛苦在於編譯部分,這需要花費最多時間,而且使用 Swift 又更加嚴重,它降低了進行快速迭代的效率,彷彿編譯器編譯時,偷偷走了去挖掘比特幣😅。

如果使用 React 開發,你就會知道它只是 UI 狀態 UI = f(state) 的表示。你會獲得一些數據,然後構建一個 UI 來呈現它。React 備有 Hot reloader 和 Storybook,使它可以快速地進行 UI 迭代,這樣你就可以進行更改,並立即查看結果,還可以全面了解每個狀態所有可能的 UI。你一定也想在 iOS 中這樣做吧!

Playground

WWDC 2014 中,Apple 除了介紹 Swift,還順勢推出了 Playground,據稱是「一種探索 Swift 編程語言的新方法」。

起初我不太相信,尤其是看到很多人抱怨它緩慢或反應遲鈍之後。但在看到 Kickstarter iOS app 使用 Playground 來加快樣式和開發流程後,給我留下了深刻的印象。所以我開始在一些應用程式中也使用 Playground,雖然它不像 React NativeInjection App 能立即重新渲染,但希望它會越來越好😇。

Playground 的場景是我們一次只設置一個螢幕或組件,這迫使我們仔細考慮依賴關係 (dependencies),所以我只能導入一個特定的螢幕,並在 Playground 中進行迭代。

Playground 內導入客製化框架

Xcode 9 允許我們在 Playground 中導入客製化的框架,只要這個框架與 Playground 位於同一工作區 (workspace) 中。我們可以使用 Carthage 來獲取並構建客製化框架,或是使用 CocoaPods 也可以。

Playground 驅動開發

創建 App 框架

如果將 Playground 添加為嵌套項目,它就無法訪問同一個工作區或父項目中的程式碼。為此,你需要創建一個框架並添加打算在 Playground 中開發的原文件 (source files),我們稱之為 App 框架。

本文範例是一個使用 CocoaPods 來管理依賴關係的 iOS 專案,在撰寫這篇文章的時候,建置環境是 Xcode 9.3 和 Swift 4.1。

讓我們逐步探討如何讓 Playground 運行使用 CocoaPods 的專案,並介紹一些練習範例。

步驟一:新增 pod 文件

我多數會使用 CocoaPods 來管理依賴關係。在某些頁面中,一定會接觸到某些 pod。因此,為了使我們的 App 框架能夠運作,就需要與一些 pod 連結。

創建一個新專案,命名為 UsingPlayground。這個 App 會顯示某種五彩紙屑粒子🎊,並有很多選項可以調整這些粒子的顯示方式,而我選擇 Playground 來迭代它。

因為我想透過這個範例做一些有趣的東西,所以我們將使用 CocoaPods 來獲取一個名為 Cheers 的依賴項。如果你想要恭喜用戶取得某些成就,Cheers 有助顯示花哨的五彩紙屑效果。

使用 UsingPlayground 創建 Podfile 為 App 的 target

 
platform :ios, ‘9.0’
use_frameworks!
pod ‘Cheers’
target ‘UsingPlayground’

步驟二:在專案中使用這個 Pod

運行 pod install 後,CocoaPods 就會建立出一個包含兩個專案項目的工作區。一個是我們的 App 專案,另一個是包含所有 pod 的專案,但是目前它只包含了 Cheers。關閉你的專案並改為打開剛建立的工作區。

這非常簡單,你只需要確保 pod 順利運作。來寫一些程式碼使用 Cheers:

 
public class ViewController: UIViewController {
  public override func viewDidLoad() {
    super.viewDidLoad()

    let cheerView = CheerView()
    view.addSubview(cheerView)
    cheerView.frame = view.bounds

    // Configure
    cheerView.config.particle = .confetti

    // Start
    cheerView.start()
  }
}

建立並運行專案,享受一下迷人的五彩紙屑🎊。

步驟三:新增 CocoaTouch 框架

為了讓程式碼可在 Playground 中訪問,我們需要將它設置為一個框架。在 iOS 中,即是 CocoaTouch Framework Target。

在工作區中選擇 UsingPlayground 專案,並添加一個新的 CocoaTouch 框架。這是包含 App 程式碼的框架,把它名命為 AppFramework

CocoaTouch framework-1

現在,把要測試的原文件添加到此框架中。目前我們只需檢查 ViewController.swift 文件,並將其添加到 AppFramework target。

CocoaTouch framework-2

這個專案十分簡單,只有一個 ViewController.swift。如果此文件引用其他文件中的程式碼,你就需要將相關文件添加到 AppFramework target 中,這是一個處理依賴關係的好方法。

步驟四:添加文件至 AppFramework

iOS 中的 ViewController 主要位於 UI 層,因此它應該只獲取解析後的數據,並使用 UI 元件進行渲染。如果裡面可能涉及如緩存、網絡…… 等其他部分的邏輯,就需要你在 AppFramework 添加更多的文件。小巧而獨立的框架會顯得更合理,亦允許我們快速迭代。

請記住,Playground 不是魔法,每次我們更改程式碼時都需要編譯 AppFramework,否則更改將不會反映在 Playground。如果不介意編譯時間過慢,你可以將所有文件添加到 AppFramework。簡單擴展分組文件夾、選擇文件、並將其添加到 target 就需要花費大量時間,更不用說如果同時選擇文件夾和文件,則無法將它們添加到 target 中;你只能將文件添加到 target。

AppFramework-1

更快捷的方法是轉到 AppFramework 的 target 中,選擇 Build Phase,然後點擊 Compile Sources。這裡所有文件都會自動展開,只需選擇所需文件並點擊Add 即可。

AppFramework-2

步驟五:Public

在預設情況下,Swift 類型和方法是 Internal。因此,為了讓它們在 Playground 中可見,我們需要將它們宣告為 Public。請在此閱讀更多關於 Swift 的訪問級別

Open 訪問和 Public 訪問允許實體被定義模塊中的任意原文件訪問,同樣可以被另一模塊的原文件通過導入該定義模塊來訪問。在指定框架的公共接口時,通常使用 Open 或 Public 訪問。

 
public class ViewController: UIViewController {
  // Your code goes here
}

步驟六:在 AppFramework 新增 pod

為了使 AppFramework 能夠使用我們的 pod,還需要將這些 pod 添加到 framework target 之中。將 target ‘AppFramework’ 添加到 Podfile

 
platform :ios, ‘9.0’
use_frameworks!
pod ‘Cheers’
target ‘UsingPlayground’
target ‘AppFramework’

現在重新執行 pod install。在極少數情況下,你會需要運行 pod deintegratepod install 來從一個全新狀態開始。

步驟七:新增一個 Playground

添加一個 Playground 並將其拖到我們的工作區,將它命名為 MyPlayground

Add Playground-1

Add Playground-2

步驟八:盡情享受

這是最後一步:編寫一些程式碼。現在:我們需要在 Playground 中導入 AppFrameworkCheers,並且把所有會在 Playground 使用的 pod 導入,就像我們在 App 專案所做的一樣。

Playground 最適合獨立測試我們的 framework 或 App,選擇 MyPlayground 並輸入以下程式碼。在這裡我們告訴 liveView 呈現 ViewController

 
import UIKit
import AppFramework
import PlaygroundSupport

let controller = ViewController()
controller.view.frame.size = CGSize(width: 375, height: 667)
PlaygroundPage.current.liveView = controller.view

如果你想測試其中一個想要使用的 pod,可以創建一個名為 CheersAlone 的新 Playground Page。你只需要在這裡導入 Cheers

New Playground Page-1

 
import UIKit
import Cheers
import PlaygroundSupport

// Use cheer alone
let cheerView = CheerView()
cheerView.frame = CGRect(x: 0, y: 50, width: 200, height: 400)

// Configure
cheerView.config.particle = .confetti(allowedShapes: [.rectangle, .circle])

// Start
cheerView.start()

PlaygroundPage.current.liveView = cheerView

讓我們使用 PlaygroundPageliveView 來顯示即時視圖。記得要切換編輯器模式,以便查看 Playground 結果。然後,🎉!

Playgrould Button

Xcode 面板的底部有一個按鈕,有了它,你就可以在 Automatically RunManual Run 行為之間切換,亦可以自己暫停或開始運行 Playground,相當簡便 🤘。

Bridging header

你的 App 可能需要處理一些預先構建的二進制 pod,它通過 header 公開 API。在某些應用中,我用了 BuddyBuildSDK 進行閃退回報 (crash reports),如果你看看它的 podspec,就會看到它使用一個名為 BuddyBuildSDK.h. 的 public header。在我們的 App 專案項目中,CocoaPods 管理得很好,你需要做的就是透過 Bridging-Header.h,在 App target 中導入 header。

如果你需要查看如何使用 bridging header,請查閱Swift and Objective-C in the Same Project 這個連結。

 
#ifndef UsingPlayground_Bridging_Header_h
#define UsingPlayground_Bridging_Header_h

#import 

#endif

只需確保 header 的路徑正確:

Path to Header

步驟一:導入 Bridging Header

但是 AppFramework target 很難找到 BuddyBuildSDK.h

Using bridging headers with framework targets is unsupported

解決方法是在 AppFramework.h 中引用 Bridging-Header.h

 
#import 

//! Project version number for AppFramework.
FOUNDATION_EXPORT double AppFrameworkVersionNumber;

//! Project version string for AppFramework.
FOUNDATION_EXPORT const unsigned char AppFrameworkVersionString[];

// In this header, you should import all the public headers of your framework using statements like #import 

#import "Bridging-Header.h"

AppFramework.h

步驟二:將 header 宣告為 public

完成上述操作後,你將看到:

Include of non-modular header inside framework module

為此,你需要將 Bridging-Header.h 添加到框架中,並將其宣告為 public,點擊此連結來看看這段說明。

Public:界面已經完成,並準備供你產品的客户端使用。產品中不受限制地將 Public Header 作為可讀原始碼包括在內。
Private: Private:該界面不是為你的客户端設計的,又或是還處於開發早期階段。產品會包含 Private Header,但會聲明為 “Privite”。因此,所有客户端都可以看到這些標記,但是應該明白,他們不應使用該界面。
Project:該界面僅供當前專案的實現文檔使用。Target 中不包含項目程式碼外的專案 Header,其標記對客户端來説不可見,只有你可以看到。

所以,讓我們選擇 Bridging-Header.h,將其添加到 AppFramework,並設置 visibility 為 public

Public Header-1

如果你點擊 AppFrameworkBuild Phases,你會看到兩個 header 文件。

Public Header-2

現在,選擇 AppFramework 並點擊 Build,它編譯時應該不會有任何錯誤。

字體、本地化字串、圖像以及 bundle

我們的螢幕不僅包含來自其他 pod 的視圖,也經常會從 Bundle 中顯示文本和圖像,讓我們將 Iron Man 圖像和 Localizable.strings 添加到 Asset Catalog。ResourceViewController 包含了一個 UIImageView 和一個 UILabel

 
import UIKit
import Anchors

public class ResourceViewController: UIViewController {
  let imageView = UIImageView()
  let label = UILabel()

  public override func viewDidLoad() {
    super.viewDidLoad()

    view.backgroundColor = UIColor.gray

    setup()
    imageView.image = UIImage(named: "ironMan")
    label.text = NSLocalizedString("ironManDescription", comment: "Can't find localised string")
  }

  private func setup() {
    imageView.contentMode = .scaleAspectFit
    label.textAlignment = .center
    label.textColor = .black
    label.font = UIFont.preferredFont(forTextStyle: .headline)
    label.numberOfLines = 0

    view.addSubview(imageView)
    view.addSubview(label)

    activate(
      imageView.anchor.width.multiplier(0.6),
      imageView.anchor.height.ratio(1.0),
      imageView.anchor.center,

      label.anchor.top.equal.to(imageView.anchor.bottom).constant(10),
      label.anchor.paddingHorizontally(20)
    )
  }
}

在這裡,我使用 Anchors 方便的宣告式自動佈局 (declarative Auto Layout) 🤘。這也是為了稍後展示 Swift Playground 如何處理任意數量的框架。

現在,選擇應用模式 UsingPlayground,點擊建置來運行它。App 應如下所示,能夠顯示正確的圖像和本地化字串。

Try Your App Now-1

看看我們的 Playground 是否可以識別這些 assets,在 MyPlayground 中創建一個名為 Resource 的新頁面,然後鍵入以下程式碼:

 
import UIKit
import AppFramework
import PlaygroundSupport

let controller = ResourceViewController()
controller.view.frame.size = CGSize(width: 375, height: 667)

PlaygroundPage.current.liveView = controller.view

等待 Playground 完成運行。哎呀,Playground 中的結果並不如我們所預期,它無法識別圖像和本地化字串😢。

Playground-1

Resources 文件夾

實際上,每個 Playground Page 都有一個 Resources 文件夾,可以放置這個特定頁面要使用的資料文件 (resource files)。但是,在這裡我們需要訪問 App bundle 中的 resource。

Main bundle

在訪問圖像和本地化字串時,如果未指定 bundle,正在運行的 App 就會預設選擇 main bundle 中的 resources,以下是 關於查找和打開 Bundle 的相關資訊。

在找到資源之前,必須先指定包含該資源的 bundle。Bundle 類型中有許多構造函數 (constructors),, 但是最常用的是 main。Main bundle 表示包含正在執行的程式碼的 Bundle 目錄。因此對於 App 來說,Main bundle 對象可以讓你訪問與 App 一起發佈的資源。

如果你的 App 直接與插件、框架或其他 bundle 內容交互,y則可以使用此類型的其他方法創建適當的 bundle 對象。

 
// Get the app's main bundle
let mainBundle = Bundle.main

// Get the bundle containing the specified private class.
let myBundle = Bundle(for: NSClassFromString("MyPrivateClass")!)

步驟一:將 resources 新增至 AppFramework target

首先,我們需要將資料文件添加到 AppFramework target 中。選擇 Asset CatalogLocalizable.strings,並添加到 AppFramework target 中。

Add Resources to AppFramework

步驟二:指定 bundle

如果我們不指定 bundle,就會預設使用 mainBundle。在執行的 Playground context 中,mainBundle 指的是其 Resources 文件夾,但我們希望 Playground 能夠存取 AppFramework 中的 resources。因此,我們需要在 AppFramework 中使用一個類別調用 useBundle.nit(for:) 方法,來引用 AppFramework 中的 bundle。該類可以是 ResourceViewController,因為它也被添加到 AppFramework target中。

ResourceViewController 中的程式碼更改為:

 
let bundle = Bundle(for: ResourceViewController.self)
imageView.image = UIImage(named: "ironMan", in: bundle, compatibleWith: nil)
label.text = NSLocalizedString(
  "ironManDescription", tableName: nil,
  bundle: bundle, value: "", comment: "Can't find localised string"
)

每次我們在 AppFramework 中更改程式碼時,都需要重新編譯它,這一點很重要。現在,打開 Playground,它應該選取到正確的 assets。

AppFramework-LiveView

客製化字體

要使用客製化字體,我們需要先註冊字體。我們可以使用 CTFontManagerRegisterFontsForURL 註冊客製化字體,來取代使用 plist 文件中 Fonts provided by application 的字體。這非常方便,因為字體也可以在 Playground 中動態註冊。

下載一個名為 Avengeance 的免費字體,並將此字體添加到 App 和 AppFramework target 中。

添加程式碼以在 ResourceViewController 中指定字體,記得重新編譯 AppFramework

 
// font
let fontURL = bundle.url(forResource: "Avengeance", withExtension: "ttf")
CTFontManagerRegisterFontsForURL(fontURL! as CFURL, CTFontManagerScope.process, nil)
let font = UIFont(name: "Avengeance", size: 30)!
label.font = font

看吧!你的 App 和 Playground 都可以看到客製化字體了 🎉!

AppFramework-test

設備尺寸和特徵集合

iOS 8 引入了 TraitCollection 來定義設備尺寸類型、縮放和用戶介面習慣用法,簡化了設備描述。kickstarter-ios 專案有一個方便的工具來準備 UIViewController,以便在 Playground 中使用不同的特性,請參閱 playgroundController

 
public func playgroundControllers(device: Device = .phone4_7inch,
                                  orientation: Orientation = .portrait,
                                  child: UIViewController = UIViewController(),
                                  additionalTraits: UITraitCollection = .init())
  -> (parent: UIViewController, child: UIViewController) {

AppEnvironment 就像一個堆疊 (Stack),可用於更改依賴關係和 App 屬性。例如 bundle、區域設置、和語言。你可以參考有關 Signup screen 的範例:

 
import Library
import PlaygroundSupport
@testable import Kickstarter_Framework

// Instantiate the Signup view controller.
initialize()
let controller = Storyboard.Login.instantiate(SignupViewController.self)

// Set the device type and orientation.
let (parent, _) = playgroundControllers(device: .phone4inch, orientation: .portrait, child: controller)

// Set the device language.
AppEnvironment.replaceCurrentEnvironment(
  language: .en,
  locale: Locale(identifier: "en") as Locale,
  mainBundle: Bundle.framework
)

// Render the screen.
let frame = parent.view.frame
PlaygroundPage.current.liveView = parent

無法查找字符

使用 Playground 時可能會出現一些錯誤,部分是因為你的程式碼,而有些是因為框架的配置方式。就我而言,在升級到 CocoaPods 1.5.0 後,我看到了這個錯誤:

error: Couldn’t lookup symbols:
__T06Cheers9CheerViewCMa
__T012AppFramework14ViewControllerCMa
__T06Cheers8ParticleO13ConfettiShapeON
__T06Cheers6ConfigVN

符號查找 (symbol lookup) 問題即是 Playground 無法找到你的程式碼,這可能是因為你的類別未設為 public,或者你忘記了將文件添加到 AppFramework target 中,又或是在 AppFramework Framework search path 中看不到引用的 pod 等原因。

1.5.0 版本使用靜態庫支援,亦改變了 modular header。與此同時,讓我們將範例切換回 CocoaPods 1.4.0,你可以看看 UsingPlayground demo

在終端機中,輸入 bundler init 以生成 Gemfile,將 cocoapods gem 指定為1.4.0:

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "cocoapods", '1.4.0'

現在運行 bundler exec pod install,從 CocoaPods 1.4.0 運行 pod 指令,問題應該就能解決。

延伸閱讀

Swift Playground 還支持 macOStvOS,如果想了解更多,請查閱下列連結:

  1. Playground 驅動開發 :查看Brandon Williams 的介紹視頻以及kickstarter-ios 專案對如何使用 Playground 來開發應用,你應該會有所啟發。此外,objc.io 關於 Playground-Driven Development 的介紹也非常好。
  2. PointFree:這個網站是在 Playground 的開發下完成的。透過閱讀程式碼和專案結構,應該獲益不少。
  3. Using Swift to Visualize Algorithms:Playground 亦可以將你的想法原型化和可視化。
  4. How to not get sand in your eyes:我的朋友 Felipe 在 GitHub 上編寫了他在工作中成功使用 Playground 的文章。
  5. Awesome PlaygroundUmberto Raimondi 列舉了一系列很棒的 Playground 專案清單,將會讓你眼前一亮。
本篇原文(標題: Playground driven development in Swift)刊登於作者 Medium,由 Khoa Pham 所著,並授權翻譯及轉載。
作者簡介:Khoa,Hyper (@hyperoslo) 的流動程式開發者,於 GitHubMedium 上寫文章。
譯者簡介:陳奕先-過去為平面財經記者,專跑產業新聞,2015 年起跨進軟體開發世界,希望在不同領域中培養新的視野,於新創學校 ALPHA Camp 畢業後,積極投入 iOS 程式開發,目前任職於國內電商公司。聯絡方式:電郵 [email protected]

FB : https://www.facebook.com/yishen.chen.54
Twitter : https://twitter.com/YeEeEsS

作者
AppCoda 編輯團隊
此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。
評論
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。