阅读 884

[译] 手把手教你用 Playground 创建 App Framework

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前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

关注下面的标签,发现更多相似文章
评论