这个轻量级框架主要包括
它还包含其他 Foundation 和 UIKit 的各种扩展。
Y—CoreUI 采用 Apache 2.0 许可。
文档自动从源代码注释生成,并呈现为通过 GitHub Pages 托管的静态网站,地址为:https://yml-org.github.io/YCoreUI/
为了辅助自动布局,Y—CoreUI 具有多个 UIView
扩展,可以简化布局约束的创建。 这些扩展没有使用任何第三方库(例如 SnapKit),而只是 Apple 自己的 NSLayoutConstraint
API 的包装器。 如果您更喜欢直接使用 Apple 的布局约束 API,那么完全可以使用它们。 但是,这些便捷方法可以减少代码量,并更直接地表达意图。
所有扩展都应用于 UIView
,并且以 constrain
开头。
最简单的形式是仅使用属性创建约束,就像最初的 iOS 6 NSLayoutContraint
API 一样。
// constrain a button's width to 100
let button = UIButton()
addSubview(button)
button.constrain(.width, constant: 100)
// constrain view to superview
let container = UIView()
addSubview(container)
container.constrain(.leading, to: .leading, of: superview)
container.constrain(.trailing, to: .trailing, of: superview)
container.constrain(.top, to: .top, of: superview)
container.constrain(.bottom, to: .bottom, of: superview)
另一种形式是使用锚点创建约束,就像 iOS 9 中首次引入的锚点 API 一样。
// constrain a button's width to 100
let button = UIButton()
addSubview(button)
button.constrain(.widthAnchor, constant: 100)
// constrain view to superview
let container = UIView()
addSubview(container)
container.constrain(.leadingAnchor, to: leadingAnchor)
container.constrain(.trailingAnchor, to: trailingAnchor)
container.constrain(.topAnchor, to: topAnchor)
container.constrain(.bottomAnchor, to: bottomAnchor)
有一些重载可以处理将一个视图放置在另一个视图下方或另一个视图的尾侧的常见用例。
// constrain button2.leadingAnchor to button1.trailingAnchor
button2.constrain(after: button1, offset: insets.leading)
// constrain label2.topAnchor to label1.bottomAnchor
label2.constrain(below: label1, offset: gap)
但这些扩展真正发挥作用的是 constrainEdges
方法,只需一次方法调用即可创建最多四个约束。
// constrain 2 buttons across in a view
let button1 = UIButton()
let button2 = UIButton()
let insets = NSDirectionalEdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)
addSubview(button1)
addSubview(button2)
button1.constrainEdges(.notTrailing, with: insets)
button2.constrainEdges(.notLeading, with: insets)
button2.constrain(after: button1, offset: insets.leading)
button1.constrain(.widthAnchor, to: button2.widthAnchor)
// constrain view to superview
let container = UIView()
addSubview(container)
container.constrainEdges()
还有一个 constrainEdgesToMargins
变体,用于在接收器的边缘和指定视图的布局边距(通常是接收器的父视图)之间设置约束。 这对于避免安全区域(例如导航栏或 FaceID 刘海所占据的区域)非常有用。
// constrain 2 buttons across in a view using margins
let button1 = UIButton()
let button2 = UIButton()
let spacing: CGFloat = 16
addSubview(button1)
addSubview(button2)
button1.constrainEdgesToMargins(.notTrailing)
button2.constrainEdgesToMargins(.notLeading)
button2.constrain(after: button1, offset: spacing)
button1.constrain(.widthAnchor, to: button2.widthAnchor)
// constrain view to superview margins
let container = UIView()
addSubview(container)
container.constrainEdgesToMargins()
有三种便捷方法可以约束视图的大小。
// constrain a button's size to 44 x 44 (3 different ways)
let button = UIButton()
addSubview(button)
button.constrainSize(CGSize(width: 44, height: 44))
button.constrainSize(width: 44, height: 44)
button.constrainSize(44)
有一个用于对齐视图的 Auto Layout 便捷方法。
// center a container view to its superview
let container = UIView()
addSubview(container)
container.constrainCenter()
// center a button horizontally
let button = UIButton()
addSubview(button)
button.constrainCenter(.x)
// align a button and a label vertically by their centers
let button = UIButton()
let label = UILabel()
addSubview(button)
addSubview(label)
button.constrainCenter(.y, to: label)
有一个用于约束宽高比的 Auto Layout 便捷方法。
// constrain to a 16:9 aspect ratio
mediaPlayer.constrainAspectRatio(16.0 / 9)
// constrain to a 1:1 aspect ratio
profileImage.constrainAspectRatio(1)
我们有一些扩展可以加速字符串、颜色和图像的加载(并使其易于单元测试)。
轻松地从任何基于字符串的 Enum
加载本地化字符串资源。 您所需要做的就是声明符合 Localizable
协议,您就可以访问 localized: String
属性。
// Conform your Enum to Localizable
enum SettingConstants: String, Localizable, CaseIterable {
case title = "Settings_Title"
case color = "Settings_Color"
}
// Then access the localized string
label.text = SettingsConstants.title.localized
单元测试也很容易
func test_Setting_Constants_loadsLocalizedString() {
SettingConstants.allCases.forEach {
// Given a localized string constant
let string = $0.localized
// it should not be empty
XCTAssertFalse(string.isEmpty)
// and it should not equal its key
XCTAssertNotEqual($0.rawValue, string)
}
}
该协议还允许您指定包含本地化字符串的 bundle 和可选的表名。
轻松地从任何基于字符串的 Enum
加载颜色资源。 您所需要做的就是声明符合 Colorable
协议,您就可以访问 color: Color
属性。 您甚至可以定义一个 fallbackColor
来代替 nil
或 .clear
,这样在发生故障时,UI 元素也不会不可见(但默认情况下它们是鲜艳的粉红色,以便引起您的注意)。
// Conform your Enum to Colorable
enum PrimaryColors: String, CaseIterable, Colorable {
case primary50
case primary100
}
// Then access the color
label.textColor = PrimaryColors.primary50.color
单元测试很容易
func test_PrimaryColors_loadsColor() {
PrimaryColors.allCases.forEach {
XCTAssertNotNil($0.loadColor())
}
}
该协议还允许您指定包含颜色资源的 bundle、可选的命名空间和 fallback 颜色。
轻松地从任何基于字符串的 Enum
加载图像资源。 您所需要做的就是声明符合 ImageAsset
协议,您就可以访问 image: UIImage
属性。 您甚至可以定义一个 fallbackImage
来代替 nil
,这样在发生故障时,UI 元素也不会不可见(但默认情况下它是一个鲜艳的粉红色正方形,以便引起您的注意)。
// Conform your Enum to ImageAsset
enum Flags: String, ImageAsset {
case unitedStates = "flag_us"
case india = "flag_in"
}
let flag: Flags = .india
// Then access the image
let image: UIImage = flag.image
如果您将 CaseIterable
添加到您的枚举中,那么编写单元测试以确保它们正常工作(并且您可以添加、更新、修改枚举 cases 而无需更新您的单元测试)就会非常简单。
enum Icons: String, CaseIterable, ImageAsset {
case value1
case value2
...
case valueLast
}
func test_iconsEnum_loadsImage() {
Icons.allCases.forEach {
XCTAssertNotNil($0.loadImage())
}
}
该协议还允许您指定包含图像资源的 bundle、可选的命名空间和 fallback 图像。
轻松地从任何基于字符串的 Enum
加载系统图像 (SF Symbols)。 您所需要做的就是声明符合 SystemImage
协议,您就可以访问 image: UIImage
属性。 与上面的 ImageAsset
类似,您可以定义一个 fallbackImage
。
既然它只是包装了 UIImage(systemName:)
,为什么还要费心这样做? 因为
UIImage(systemName:)
返回 UIImage?
,而 SystemImage.image
返回 UIImage
。SystemImage.image
返回随动态类型缩放的图像。// Conform your Enum to SystemImage
enum Checkbox: String, SystemImage {
case checked = "checkmark.square"
case unchecked = "square"
}
// Then access the image
button.setImage(Checkbox.unchecked.image, for: .normal)
button.setImage(Checkbox.checked.image, for: .selected)
如果您将 CaseIterable
添加到您的枚举中,那么编写单元测试以确保它们正常工作(并且您可以添加、更新、修改枚举 cases 而无需更新您的单元测试)就会非常简单。
enum Checkbox: String, CaseIterable, SystemImage {
case checked = "checkmark.square"
case unchecked = "square"
}
func test_checkboxEnum_loadsImage() {
Checkbox.allCases.forEach {
XCTAssertNotNil($0.loadImage())
}
}
Y—CoreUI 包含许多扩展,可以更轻松地处理颜色。 其中最有用的可能是 WCAG 2.0 对比度计算。 给定任意两种颜色(代表前景色和背景色),您可以计算它们之间的对比度,并评估是否通过特定的 WCAG 2.0 标准(AA 或 AAA)。 您甚至可以编写单元测试来快速检查应用程序中所有颜色模式下的所有颜色对。 看起来像这样
final class ColorsTests: XCTestCase {
typealias ColorInputs = (foreground: UIColor, background: UIColor, context: WCAGContext)
// These pairs should pass WCAG 2.0 AA
let colorPairs: [ColorInputs] = [
// label on system background
(.label, .systemBackground, .normalText),
// label on secondary background
(.label, .secondarySystemBackground, .normalText),
// label on tertiary background
(.label, .tertiarySystemBackground, .normalText),
// secondary label on system background
(.secondaryLabel, .systemBackground, .normalText),
// secondary label on secondary background
(.secondaryLabel, .secondarySystemBackground, .normalText),
// secondary label on tertiary background
(.secondaryLabel, .tertiarySystemBackground, .normalText),
// tertiary label on system background
(.tertiaryLabel, .systemBackground, .normalText),
// tertiary label on secondary background
(.tertiaryLabel, .secondarySystemBackground, .normalText),
// tertiary label on tertiary background
(.tertiaryLabel, .tertiarySystemBackground, .normalText),
// system red on system background (fails)
// (.systemRed, .systemBackground, .normalText),
]
let allColorSpaces: [UITraitCollection] = [
// Light Mode
UITraitCollection(userInterfaceStyle: .light),
// Light Mode, Increased Contrast
UITraitCollection(traitsFrom: [
UITraitCollection(userInterfaceStyle: .light),
UITraitCollection(accessibilityContrast: .high)
]),
// Dark Mode
UITraitCollection(userInterfaceStyle: .dark),
// Dark Mode, Increased Contrast
UITraitCollection(traitsFrom: [
UITraitCollection(userInterfaceStyle: .dark),
UITraitCollection(accessibilityContrast: .high)
])
]
func testColorContrast() {
// test across all color modes we support
for traits in allColorSpaces {
// test each color pair
colorPairs.forEach {
let color1 = $0.foreground.resolvedColor(with: traits)
let color2 = $0.background.resolvedColor(with: traits)
XCTAssertTrue(
color1.isSufficientContrast(to: color2, context: $0.context, level: .AA),
String(
format: "#%@ vs #%@ ratio = %.02f under %@ Mode%@",
color1.rgbDisplayString(),
color2.rgbDisplayString(),
color1.contrastRatio(to: color2),
traits.userInterfaceStyle == .dark ? "Dark" : "Light",
traits.accessibilityContrast == .high ? " Increased Contrast" : ""
)
)
}
}
}
}
FormViewController
是一个具有可滚动内容区域的视图控制器,可以自动为您避免键盘。 对于具有输入的视图(例如,登录或 onboarding)来说,这是一个不错的选择。 即使对于没有输入的视图,它对于管理 UIScrollView
的创建和其中设置的 contentView
仍然非常有用,因此您可以专注于您的内容,而无需为每个视图编写 scrollView 代码。
想要拥有一个可以避免键盘的 scrollview,但您无法使用 FormViewController
? 它的大部分功能只是 UIScrollView
的一个简单扩展。 您可以像这样将键盘避免功能添加到任何滚动视图:
scrollView.registerKeyboardNotifications()
Elevation
是一个模型对象,用于定义阴影,类似于 W3C box-shadows 和 Figma drop shadows。 它具有以下参数,这些参数与 Figma(和 web)定义 drop shadows 的方式相匹配
Elevation
具有一个 apply
方法,该方法然后将阴影效果应用于 CALayer
。 记住每次颜色模式更改时都调用它,以更新阴影颜色 (CGColor
)。
let button = UIButton()
let elevation = Elevation(
xOffset: 0,
yOffset: 2,
blur: 5,
spread: 0,
color: .black,
opacity: 0.5
)
elevation.apply(layer: button.layer, cornerRadius: 8)
Animation
是一个模型对象,用于定义 UIView 动画。 它具有以下参数
Animation.curve
是一个具有关联值的枚举,可以是 .regular
或 .spring
。
有一个 UIView
类重写方法 animate
,它接受一个 Animation
对象。
采用 Animation
结构的好处是,使用一个方法,您可以对常规或 spring 动画进行动画处理。 这允许我们构建组件,用户可以自定义使用的动画,而无需使我们的代码过于复杂或脆弱。
let button = UIButton()
button.alpha = 1
let animation = Animation(duration: 0.25, curve: .regular(options: .curveEaseOut))
UIView.animate(with: animation) {
// fade button out
button.alpha = 0
} completion: {
// remove it from the superview when done
button.removeFromSuperview()
}
您可以通过将 Y-CoreUI 作为包依赖项添加到 Xcode 项目中来添加它。
brew install swiftlint
sudo gem install jazzy
克隆 repo 并在 Xcode 中打开 Package.swift
。
我们使用 语义版本控制。
{major}.{minor}.{patch}
例如
1.0.5
我们为我们的框架使用简化的分支策略。
main
main
分支出来main
。main
会被标记上每个发布的更新版本号。feature/{ticket-number}-{short-description}
bugfix/{ticket-number}-{short-description}
例如
feature/CM-44-button
bugfix/CM-236-textview-color
在提交 pull request 之前,您应该
swiftlint
并确认没有违规行为。jazzy
并确认您有 100% 的文档覆盖率。git rebase -i HEAD~{commit-count}
将您最近的 {commit-count} 个提交合并成功能块。main
)的 HEAD 自您创建分支以来已更新,请使用 git rebase main
来变基您的分支。提交 pull request 时
合并 pull request 时
1.0.5
)你可以直接从源代码生成你自己的本地文档集,只需在终端中使用以下命令:
jazzy
这会在 /docs
目录下生成一套文档。默认配置位于默认配置文件 .jazzy.yaml
中。
要查看更多文档选项,请输入:
jazzy --help
每次向 main
分支推送提交时,GitHub Action 会自动运行 Jazzy,为我们的 GitHub 页面生成文档,地址为:https://yml-org.github.io/YCoreUI/