- 原文地址:Playground driven development in Swift
- 原文做者:Khoa Pham
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:ALVINYEH
- 校對者:swants、talisk
經過咱們開發的 app,爲用戶提供最佳使用體驗,讓生活變得更便利,更豐富多彩,是咱們做爲移動開發者的天生使命。其中咱們要作的一件事就是確保爲用戶展示的 UI 看起來很棒而且不存在絲毫問題。在大多數狀況下,app 能夠說是數據的美容師。咱們經常從後端獲取 json,解析爲 model,並經過 UIView(大多數狀況下是 UITableView 或 UICollectionView)將數據渲染出來。html
對於 iOS,咱們須要根據設計來不斷調整用戶界面,使其可以適合小尺寸的手持設備。這個過程涉及到更改代碼、編譯、等待、檢查、而後又更改代碼等等……像 Flawless App 這樣的工具能夠幫助你輕鬆地比對 iOS 應用和 Sketch 設計的結果。但真正痛苦的是編譯部分,這個過程須要花大量的時間,而對於 Swift 來講,狀況就更加糟糕了。由於它會下降咱們快速迭代的效率。感受編譯器像是在編譯時偷偷挖礦。😅前端
若是你使用 React,你就知道它僅僅是狀態 UI = f(state).
的一個 UI 表示。你會獲得一些數據,而後建立一個 UI 來呈現它。React 具備 hot reloader 和 Storybook,因此 UI 迭代會很是快。你只要進行一些改變,當即能夠看到結果。你還能夠得到所有可能使用的 UI 各類狀態的完整概述。你心裏深知本身也想在原生 iOS 中這樣作!react
除了在 2014 年 WWDC 推出了 Swift 外,蘋果還推出了 Playground,聽說這是「一種探索 Swift 變成語言的新穎創新方式」。android
起初我並不十分相信,而且我看到不少關於 Playground 反應緩慢或無反應的抱怨。但當我看到 Kickstarter iOS 應用使用 Playground 來加速其樣式和開發流程後,它給我留下了深入的印象。因此我開始在一些應用中也成功使用了 Playground。它不像 React Native 或 Injection App 那樣可以當即從新渲染,但但願它之後會愈來愈好。 😇ios
或者至少它取決於開發社區。Playground 的使用場景是咱們一次只設計一個屏幕或組件。這就須要咱們仔細考慮好依賴關係,所以我只能導入一個特定的屏幕,而後在 Playground 中進行迭代。git
Xcode 9 容許開發者在 Playground 中導入自定義 framework,只要 framework 和 Playground 在同一工做區內。咱們可使用 Carthage 來獲取並構建自定義 framework。但若是你使用的是 CocoaPods,那麼也是沒有問題的。github
若是 Playground 做爲嵌套項目添加,Playground 沒法訪問同一工做區或父項目中的代碼。爲此,你須要建立一個框架,而後添加在你打算在 Playground 中開發的源文件。咱們稱之爲應用框架。json
本文的演示是一個使用 CocoaPods 管理依賴的 iOS 工程。在編寫此文時候,使用的是 Xcode 9.3 和 Swift 4.1。swift
讓咱們經過使用 CocoPods 的項目來完成 Playground 的開發工做。這裏還有一些好的作法。後端
我主要使用 CocoaPods 來管理依賴關係。在一些屏幕中,確定會涉及一些 pod。因此爲了咱們的應用框架可以正常工做,它須要連接一些 pod。
新建一個工程項目,命名爲 UsingPlayground
。該應用顯示一些五彩紙屑顆粒 🎊。有不少選項能夠調整這些粒子顯示的方式,而且我選擇 Playground 來對其進行迭代。
對於該示例,由於想要加入一些有趣的東西,咱們將使用 CocoaPods 來獲取一個名爲 Cheers 的依賴項。若是你想慶祝用戶達成一些成就時,Cheers
能夠顯示花哨的五彩紙屑效果。
使用 UsingPlayground
建立 Podfile
做爲應用的 target:
platform :ios, ‘9.0’
use_frameworks!
pod ‘Cheers’
target ‘UsingPlayground’
複製代碼
運行 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()
}
}
複製代碼
構建並運行工程,享受這些很是迷人的紙屑吧。🎊
爲了在 Playground 中能夠訪問咱們的代碼,咱們須要將其設置爲一個框架。在 iOS 中,它是 CocoaTouch 框架的 target。
在 workspace 中選擇 UsingPlayground
項目,而後添加一個新的 CocoaTouch 框架。這個框架包含了咱們的應用程序代碼。咱們命名爲 AppFramework
。
如今將要測試的源文件添加到此框架中。如今,只需檢查 ViewController.swift
文件並將其添加到 AppFramework
的 target 中。
這個簡單的項目,如今還只有一個 ViewController.swift
。若是此文件引用了其餘文件的代碼,則還須要將相關文件添加到 AppFramework
的 target 中去。這是一個處理依賴時的好方法。
iOS 中 的 ViewController
主要位於 UI 層,所以它應該只獲取解析過的數據並使用 UI 組件渲染出來。若是當中有一些可能涉及緩存、網絡等其餘部分的邏輯,這就須要你添加更多的文件到 AppFramework。小巧且獨立的框架會顯得更合理,由於可讓咱們快速迭代。
Playground 不是魔法。你每次更改代碼時都須要編譯 AppFramework,不然沒法在 Playground 中看到更改後的效果。若是你不介意編譯時間太慢,則能夠將全部文件添加到 AppFramework
。簡單地展開組文件夾,選擇和添加文件到 target 須要不少時間。更況且,若是你選擇文件夾和文件,你將沒法將它們添加到 target,只能單獨添加文件。
更快的方式是在 AppFramework
的 target 中選擇 Build Phase
,而後點擊 Compile Sources
。在這裏,全部文件都會自動展開,你所須要作的就是選擇它們並單擊 Add
。
Swift 類型和方法默認是 internal。因此爲了讓它們在 Playground 裏可見,咱們須要將其聲明爲 public 類型。歡迎閱讀更多關於 Swift 訪問級別的信息:
開放訪問和公共訪問使實體能夠在其定義模塊中的任何源文件中使用,也能夠在導入定義模塊的另外一個模塊的源文件中使用。在爲框架指定公共接口時,一般使用開放或公開訪問。
public class ViewController: UIViewController {
// 你的代碼
}
複製代碼
爲了讓 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 並將其拖到 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
複製代碼
只須要確保頭文件的路徑是正確的:
可是 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"
複製代碼
在完成上述工做後,你會獲得
包括在框架模塊中的非模塊頭文件
爲此,你須要將 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 中並非那麼好,它不能識別圖像和本地化的字符串。😢
實際上,每一個 Playground Page
中都有一個 Resources
文件夾,咱們能夠在其中放置這個特定頁面所看到的資源文件。可是,咱們須要訪問應用程序包中的資源。
當訪問圖像和本地化字符串時,若是你不指定 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")!)
複製代碼
首先,咱們須要在 AppFramework target 添加資源文件。選擇 Asset Catalog
和 Localizable.strings
並將它們添加到 AppFramework
target。
若是咱們不指定 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
系統。若是你想了解更多,這裏有一些有趣的連接。
感謝 Lisa Dziuba。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。