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

Swift 中的 Playground 驅動開發

快速調整 UI 的需求

經過咱們開發的 app,爲用戶提供最佳使用體驗,讓生活變得更便利,更豐富多彩,是咱們做爲移動開發者的天生使命。其中咱們要作的一件事就是確保爲用戶展示的 UI 看起來很棒而且不存在絲毫問題。在大多數狀況下,app 能夠說是數據的美容師。咱們經常從後端獲取 json,解析爲 model,並經過 UIView(大多數狀況下是 UITableView 或 UICollectionView)將數據渲染出來。html

對於 iOS,咱們須要根據設計來不斷調整用戶界面,使其可以適合小尺寸的手持設備。這個過程涉及到更改代碼、編譯、等待、檢查、而後又更改代碼等等……像 Flawless App 這樣的工具能夠幫助你輕鬆地比對 iOS 應用和 Sketch 設計的結果。但真正痛苦的是編譯部分,這個過程須要花大量的時間,而對於 Swift 來講,狀況就更加糟糕了。由於它會下降咱們快速迭代的效率。感受編譯器像是在編譯時偷偷挖礦。😅前端

若是你使用 React,你就知道它僅僅是狀態 UI = f(state). 的一個 UI 表示。你會獲得一些數據,而後建立一個 UI 來呈現它。React 具備 hot reloaderStorybook,因此 UI 迭代會很是快。你只要進行一些改變,當即能夠看到結果。你還能夠得到所有可能使用的 UI 各類狀態的完整概述。你心裏深知本身也想在原生 iOS 中這樣作!react

Playground

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

起初我並不十分相信,而且我看到不少關於 Playground 反應緩慢或無反應的抱怨。但當我看到 Kickstarter iOS 應用使用 Playground 來加速其樣式和開發流程後,它給我留下了深入的印象。因此我開始在一些應用中也成功使用了 Playground。它不像 React NativeInjection App 那樣可以當即從新渲染,但但願它之後會愈來愈好。 😇ios

或者至少它取決於開發社區。Playground 的使用場景是咱們一次只設計一個屏幕或組件。這就須要咱們仔細考慮好依賴關係,所以我只能導入一個特定的屏幕,而後在 Playground 中進行迭代。git

Playground 中的自定義 framework

Xcode 9 容許開發者在 Playground 中導入自定義 framework,只要 framework 和 Playground 在同一工做區內。咱們可使用 Carthage 來獲取並構建自定義 framework。但若是你使用的是 CocoaPods,那麼也是沒有問題的。github

建立 App Framework

若是 Playground 做爲嵌套項目添加,Playground 沒法訪問同一工做區或父項目中的代碼。爲此,你須要建立一個框架,而後添加在你打算在 Playground 中開發的源文件。咱們稱之爲應用框架。json

本文的演示是一個使用 CocoaPods 管理依賴的 iOS 工程。在編寫此文時候,使用的是 Xcode 9.3 和 Swift 4.1。swift

讓咱們經過使用 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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索