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