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