- 原文地址:Playground driven development in Swift
- 原文作者:Khoa Pham
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:ALVINYEH
- 校對者:swants、talisk
Swift 中的 Playground 驅動開發
快速調整 UI 的需求
通過我們開發的 app,為使用者提供最佳使用體驗,讓生活變得更便利,更豐富多彩,是我們作為移動開發者的天生使命。其中我們要做的一件事就是確保為使用者展現的 UI 看起來很棒並且不存在絲毫問題。在大多數情況下,app 可以說是資料的美容師。我們常常從後端獲取 json,解析為 model,並通過 UIView(大多數情況下是 UITableView 或 UICollectionView)將資料渲染出來。
對於 iOS,我們需要根據設計來不斷調整使用者介面,使其能夠適合小尺寸的手持裝置。這個過程涉及到更改程式碼、編譯、等待、檢查、然後又更改程式碼等等……像 Flawless App 這樣的工具可以幫助你輕鬆地比對 iOS 應用和 Sketch 設計的結果。但真正痛苦的是編譯部分,這個過程需要花大量的時間,而對於 Swift 來說,情況就更加糟糕了。因為它會降低我們快速迭代的效率。感覺編譯器像是在編譯時偷偷挖礦。?
如果你使用 React,你就知道它僅僅是狀態 UI = f(state).
的一個 UI 表示。你會得到一些資料,然後建立一個 UI 來呈現它。React 具有 hot reloader 和 Storybook,所以 UI 迭代會非常快。你只要進行一些改變,立即可以看到結果。你還可以獲得全部可能使用的 UI 各種狀態的完整概述。你內心深知自己也想在原生 iOS 中這樣做!
Playground
除了在 2014 年 WWDC 推出了 Swift 外,蘋果還推出了 Playground,據說這是“一種探索 Swift 變成語言的新穎創新方式”。
起初我並不十分相信,並且我看到很多關於 Playground 反應緩慢或無反應的抱怨。但當我看到 Kickstarter iOS 應用使用 Playground 來加速其樣式和開發流程後,它給我留下了深刻的印象。所以我開始在一些應用中也成功使用了 Playground。它不像 React Native 或 Injection App 那樣能夠立即重新渲染,但希望它以後會越來越好。 ?
或者至少它取決於開發社群。Playground 的使用場景是我們一次只設計一個螢幕或元件。這就需要我們仔細考慮好依賴關係,因此我只能匯入一個特定的螢幕,然後在 Playground 中進行迭代。
Playground 中的自定義 framework
Xcode 9 允許開發者在 Playground 中匯入自定義 framework,只要 framework 和 Playground 在同一工作區內。我們可以使用 Carthage 來獲取並構建自定義 framework。但如果你使用的是 CocoaPods,那麼也是沒有問題的。
建立 App Framework
如果 Playground 作為巢狀專案新增,Playground 無法訪問同一工作區或父專案中的程式碼。為此,你需要建立一個框架,然後新增在你打算在 Playground 中開發的原始檔。我們稱之為應用框架。
本文的演示是一個使用 CocoaPods 管理依賴的 iOS 工程。在編寫此文時候,使用的是 Xcode 9.3 和 Swift 4.1。
讓我們通過使用 CocoPods 的專案來完成 Playground 的開發工作。這裡還有一些好的做法。
第一步:新增 pod 檔案
我主要使用 CocoaPods 來管理依賴關係。在一些螢幕中,肯定會涉及一些 pod。所以為了我們的應用框架能夠正常工作,它需要連結一些 pod。
新建一個工程專案,命名為 UsingPlayground
。該應用顯示一些五彩紙屑顆粒 ?。有很多選項可以調整這些粒子顯示的方式,並且我選擇 Playground 來對其進行迭代。
對於該示例,因為想要加入一些有趣的東西,我們將使用 CocoaPods 來獲取一個名為 Cheers 的依賴項。如果你想慶祝使用者達成一些成就時,Cheers
可以顯示花哨的五彩紙屑效果。
使用 UsingPlayground
建立 Podfile
作為應用的 target:
platform :ios, ‘9.0’
use_frameworks!
pod ‘Cheers’
target ‘UsingPlayground’
複製程式碼
第二步:在你的應用專案中使用 pod
執行 pod install
後,CocoaPods 會生成一個包含 2 個工程的 workspace 檔案。一個是我們的 App 工程,另一個是目前只包含了 Cheers
的工程。現在的話只有 Cheers
。關閉你現在的工程,改為開啟剛生成的 workspace 檔案。
這非常簡單,只是為了確保 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 框架的 target。
在 workspace 中選擇 UsingPlayground
專案,然後新增一個新的 CocoaTouch 框架。這個框架包含了我們的應用程式程式碼。我們命名為 AppFramework
。
現在將要測試的原始檔新增到此框架中。現在,只需檢查 ViewController.swift
檔案並將其新增到 AppFramework
的 target 中。
這個簡單的專案,現在還只有一個 ViewController.swift
。如果此檔案引用了其他檔案的程式碼,則還需要將相關檔案新增到 AppFramework
的 target 中去。這是一個處理依賴時的好方法。
第四步:將檔案新增到 AppFramework
iOS 中 的 ViewController
主要位於 UI 層,因此它應該只獲取解析過的資料並使用 UI 元件渲染出來。如果當中有一些可能涉及快取、網路等其他部分的邏輯,這就需要你新增更多的檔案到 AppFramework。小巧且獨立的框架會顯得更合理,因為可以讓我們快速迭代。
Playground 不是魔法。你每次更改程式碼時都需要編譯 AppFramework,否則無法在 Playground 中看到更改後的效果。如果你不介意編譯時間太慢,則可以將所有檔案新增到 AppFramework
。簡單地展開組資料夾,選擇和新增檔案到 target 需要很多時間。更何況,如果你選擇資料夾和檔案,你將無法將它們新增到 target,只能單獨新增檔案。
更快的方式是在 AppFramework
的 target 中選擇 Build Phase
,然後點選 Compile Sources
。在這裡,所有檔案都會自動展開,你所需要做的就是選擇它們並單擊 Add
。
第五步:宣告為 public 型別
Swift 型別和方法預設是 internal。所以為了讓它們在 Playground 裡可見,我們需要將其宣告為 public 型別。歡迎閱讀更多關於 Swift 訪問級別的資訊:
開放訪問和公共訪問使實體可以在其定義模組中的任何原始檔中使用,也可以在匯入定義模組的另一個模組的原始檔中使用。在為框架指定公共介面時,通常使用開放或公開訪問。
public class ViewController: UIViewController {
// 你的程式碼
}
複製程式碼
第六步:將 pod 新增到 AppFramework
為了讓 AppFramework
能夠使用我們的 pod,還需要將這些 pod 新增到框架的 target 中。在你的 Podfile
檔案中新增 target ‘AppFramework’
:
platform :ios, ‘9.0’
use_frameworks!
pod ‘Cheers’
target ‘UsingPlayground’
target ‘AppFramework’
複製程式碼
現在再次執行 pod install
。在極少數的情況下,你需要執行 pod deintegrate
和 pod install
以保證從乾淨的版本開始。
第七步: 新增一個 Playground
新增 Playground 並將其拖到 workspace 中。命名為 MyPlayground
。
第八步:盡情享受
現在來到了最後一步:編寫一些程式碼。在這裡我們需要在 Playground 匯入 AppFramework
和 Cheers
。我們需要像在應用工程中一樣,匯入 Playground 中所有使用的 Pod。
Playground 能夠最好地測試我們的獨立框架或應用。選擇 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
即可。
import UIKit
import Cheers
import PlaygroundSupport
// 單獨使用 cheer
let cheerView = CheerView()
cheerView.frame = CGRect(x: 0, y: 50, width: 200, height: 400)
// 配置
cheerView.config.particle = .confetti(allowedShapes: [.rectangle, .circle])
// 開始
cheerView.start()
PlaygroundPage.current.liveView = cheerView
複製程式碼
使用 PlaygroundPage
的 liveView 來顯示實時檢視。切記切換為編輯器模式,以便你可以看到 Playground 的結果,接著 ?。
Xcode 底部皮膚上有一個按鈕。這是你可以在 Automatically Run
和 Manual Run
之間切換的地方。你可以手動停止和開始 Playground。非常的簡潔!?
橋接標頭檔案
你的應用也許要處理一些預構建的二進位制的 pod,它們需要通過標頭檔案將 API 暴露出去。在一些應用中,我使用了 BuddyBuildSDK 來檢視崩潰日誌。如果你看下它的 podspec,你會發現它使用了一個名為 BuddyBuildSDK.h
的標頭檔案。在我們的應用中,CocoaPods 管理得很好。你所需要做的是通過 Bridging-Header.h
在你的應用 target 中匯入標頭檔案。
如果你需要檢視如何使用橋接標頭檔案,可以閱讀同一專案中的 Swift 和 Objective-C。
#ifndef UsingPlayground_Bridging_Header_h
#define UsingPlayground_Bridging_Header_h
#import <BuddyBuildSDK/BuddyBuildSDK.h>
#endif
複製程式碼
只需要確保標頭檔案的路徑是正確的:
步驟 1:匯入橋接標頭檔案
但是 AppFramework
的 target 不容易找到 BuddyBuildSDK.h
。
不支援使用帶有框架 target 的橋接標頭檔案
解決辦法是在 AppFramework.h
檔案中引用 Bridging-Header.h
。
#import <UIKit/UIKit.h>
//! AppFramework 的專案版本號。
FOUNDATION_EXPORT double AppFrameworkVersionNumber;
//! AppFramework的專案版本字串。
FOUNDATION_EXPORT const unsigned char AppFrameworkVersionString[];
// 在這個標頭檔案中,你可以像 #import <AppFramework/PublicHeader.h> 這樣匯入你框架中所需的全部公共標頭檔案
#import "Bridging-Header.h"
複製程式碼
步驟 2:將標頭檔案宣告為 public
在完成上述工作後,你會得到
包括在框架模組中的非模組標頭檔案
為此,你需要將 Bridging-Header.h
新增到框架中,並且宣告為 public
。搜尋下 SO,你就會看到這些:
Public: 介面已經完成,並打算供你的產品的客戶端使用。產品中不受限制地將公共標頭檔案作為可讀原始碼包括在內。
Private: 該介面不是為你的客戶端設計的,或者是還處於開發的早期階段。私有標頭檔案會包含在產品中,但會宣告為 “privite”。因此,所有客戶端都可以看到這些標記,但是應該明白,不應該使用它們。
Project: 該介面僅供當前專案中的實現檔案使用。專案標頭檔案不包含在 target 中,專案程式碼除外。這些標記對客戶端來說不可見,只對你有用。
所以,選擇 Bridging-Header.h
並將其新增到 AppFramework
中,並將可見性設定為 public
:
如果你點開 AppFramework
的 Build Phases
,你會看到有 2 個標頭檔案。
現在,選擇 AppFramework
然後點選 Build
,工程應該可以無錯地編譯成功。
字型、本地化字串、圖片以及包
我們的螢幕不會只是簡單地包括其他 pod 的檢視。更多的時候,我們顯示來自包中的文字和圖片。在 Asset Catalog
中加入一張鋼鐵俠的圖片和 Localizable.strings
檔案。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 方便的宣告式自動佈局?。這也是為了展示 Swift 的 Playground 如何處理任意數量的框架。
現在,選擇應用模式 UsingPlayground
並點選構建和執行。App 會變成如下所示,能夠正確地顯示了影像和本地化的字串。
讓我們看看 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 中並不是那麼好,它不能識別影像和本地化的字串。?
Resources 資料夾
實際上,每個 Playground Page
中都有一個 Resources
資料夾,我們可以在其中放置這個特定頁面所看到的資原始檔。但是,我們需要訪問應用程式包中的資源。
Main bundle
當訪問影像和本地化字串時,如果你不指定 bundle
,正在執行的應用將預設選取 Main bundle 中的資源。以下是更多關於查詢和開啟 Bundle 的更多資訊。
在找到資源之前,必須先指定包含該資源的 bundle。
Bundle
類中有許多建構函式,但是最常用的是[main](https://developer.apple.com/documentation/foundation/bundle/1410786-main)
函式。Main bundle 表示包含當前正在執行的程式碼的包目錄。因此對於應用,Main bundle 物件可以讓你訪問與應用一起釋出的資源。
如果應用直接與外掛、框架或其他 bundle 內容互動,則可以使用此類的其他方法建立適當的 bundle 物件。
// 獲取應用的 main bundle
let mainBundle = Bundle.main
// 獲取包含指定私有類的 bundle
let myBundle = Bundle(for: NSClassFromString("MyPrivateClass")!)
複製程式碼
步驟 1:在 AppFramework target 中新增資源
首先,我們需要在 AppFramework target 新增資原始檔。選擇 Asset Catalog
和 Localizable.strings
並將它們新增到 AppFramework
target。
步驟 2:指定 bundle
如果我們不指定 bundle,那麼預設會使用 mainBundle
。在執行的 Playground 的上下文中,mainBundle
指的是其 Resources
資料夾。但我們希望 Playground 訪問 AppFramework 中的資源,所以我們需要在 AppFramework
中使用一個類呼叫 [Bundle.init(for:)](https://developer.apple.com/documentation/foundation/bundle/1417717-init)
方法來引用 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,應該能找到正確的資原始檔了。
那麼自定義字型呢?
我們需要註冊字型才能使用。我們可以使用 CTFontManagerRegisterFontsForURL
來註冊自定義字型,而不是使用 plist 檔案中 Fonts provided by application
提供的字型。這很方便,因為字型也可以在 Playground 中動態註冊。
下載一個名為 Avengeance 的免費字型,新增到應用和 AppFramework
target 中。
在 ResourceViewController
中新增指定字型的程式碼,記得重新編譯 AppFramework
:
// 字型
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
複製程式碼
接著,你可以在應用和 Playground 中看見自定義字型。?
裝置尺寸和特徵集合
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 像是一個堆疊,可以改變依賴,應用屬性,如 bundle、區域設定和語言。參考一個關於註冊頁面的例子:
import Library
import PlaygroundSupport
@testable import Kickstarter_Framework
// 例項化註冊檢視控制器
initialize()
let controller = Storyboard.Login.instantiate(SignupViewController.self)
// 設定裝置型別和方向
let (parent, _) = playgroundControllers(device: .phone4inch, orientation: .portrait, child: controller)
// 設定裝置語言
AppEnvironment.replaceCurrentEnvironment(
language: .en,
locale: Locale(identifier: "en") as Locale,
mainBundle: Bundle.framework
)
// 渲染螢幕
let frame = parent.view.frame
PlaygroundPage.current.liveView = parent
複製程式碼
無法查詢字元
使用 Playground 過程中可能會出現一些錯誤。其中一些是因為你的程式碼編寫問題,一些是配置框架的方式。當我升級到 CocoaPods 1.5.0,我碰到:
error: Couldn’t lookup symbols:
__T06Cheers9CheerViewCMa
__T012AppFramework14ViewControllerCMa
__T06Cheers8ParticleO13ConfettiShapeON
__T06Cheers6ConfigVN
複製程式碼
符號查詢問題意味著 Playground 無法找到你的程式碼。這可能是因為你的類沒有宣告為 public,或者你忘記新增檔案到 AppFramework
target。又或者 AppFramework
和 Framework search path
無法找到引用的 pod 等等。
1.5.0 的版本支援了靜態庫,也改變了模組標頭檔案。與此同時,將演示的例子切換回 CocoaPods 1.4.0
,你可以看下 UsingPlayground demo。
在終端中,輸入 bundler init
來生成 Gemfile
檔案。將 gem cocoapods
設定為 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 同時支援 macOS
和 tvOS
系統。如果你想了解更多,這裡有一些有趣的連結。
- Playground Driven Development:Brandon Williams 的簡報以及 kickstarter-ios 專案對如何使用 Playground 來開發應用會有所啟發。此外,在 objc.io 關於 Playground-Driven Development 的談話也非常不錯。
- PointFree:該網站在 Playground 下開發完成的。通過閱讀程式碼和他們的專案結構,你可以學到很多東西。
- Using Swift to Visualize Algorithms:Playground 也將你的想法原型化和視覺化。
- 我的朋友 Felipe 在 How to not get sand in your eyes 上還編寫了他是如何在工作中成功使用 Playground 的文章。
- 同時,如果你想眼前一亮,Umberto Raimondi 列舉了一系列關於 Swift 的 Awesome Playground 專案清單。
感謝 Lisa Dziuba。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。