[譯] 手把手教你用 Playground 建立 App Framework

weixin_34253539發表於2018-05-25

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 reloaderStorybook,所以 UI 迭代會非常快。你只要進行一些改變,立即可以看到結果。你還可以獲得全部可能使用的 UI 各種狀態的完整概述。你內心深知自己也想在原生 iOS 中這樣做!

Playground

除了在 2014 年 WWDC 推出了 Swift 外,蘋果還推出了 Playground,據說這是“一種探索 Swift 變成語言的新穎創新方式”。

起初我並不十分相信,並且我看到很多關於 Playground 反應緩慢或無反應的抱怨。但當我看到 Kickstarter iOS 應用使用 Playground 來加速其樣式和開發流程後,它給我留下了深刻的印象。所以我開始在一些應用中也成功使用了 Playground。它不像 React NativeInjection 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 deintegratepod install 以保證從乾淨的版本開始。

第七步: 新增一個 Playground

新增 Playground 並將其拖到 workspace 中。命名為 MyPlayground

第八步:盡情享受

現在來到了最後一步:編寫一些程式碼。在這裡我們需要在 Playground 匯入 AppFrameworkCheers。我們需要像在應用工程中一樣,匯入 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。新建一個名為 CheersAlonePlayground 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
複製程式碼

使用 PlaygroundPageliveView 來顯示實時檢視。切記切換為編輯器模式,以便你可以看到 Playground 的結果,接著 ?。

Xcode 底部皮膚上有一個按鈕。這是你可以在 Automatically RunManual 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

如果你點開 AppFrameworkBuild 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 CatalogLocalizable.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。又或者 AppFrameworkFramework 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 同時支援 macOStvOS 系統。如果你想了解更多,這裡有一些有趣的連結。

感謝 Lisa Dziuba


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章