[toc]
前言
本文来自拉勾网课程整理
在上一模块“配置与规范”
中,主要介绍了如何统一项目的配置,以及如何制定统一开发和设计规范。
接下来我们将进入基础组件设计模块,我会为你介绍一些在iOS
开发过程中,工程化实践需要用的组件,比如设计组件、路由组件
。除此之外,我还会聊聊在开发中如何支持多语言、动态字体和深色模式等辅助功能,让你的App
既有国际范,获取更多用户,还能提升用户体验,获得更多好评。
这一讲,我们就先来聊聊公共组件库,以及如何封装基础设计组件。
封装公共功能组件库
随着产品不断发展,我们会发现,越来越多的公共功能可以封装成组件库,从而被各个模块甚至多个App
共同使用,比如字体、调色板、间距
和头像
可以封装成UI
设计组件库,登录会话和权限管理可以封装成登录与鉴权组件库。
通过利用这些公共功能组件库
,不仅能节省大量开发时间,不需要我们再为每个模块重复实现类似的功能;还能减少编译时间,因为如果没有独立的组件库,一点代码的改动都会导致整个 App
重新编译与链接。
那么,怎样才能创建和使用公共功能组件库呢?下面我们以一个设计组件库 DesignKit
为例子介绍下具体怎么做。
创建内部公共功能组件库
公共功能组件库根据使用范围可以分为三大类:内部库、私有库和开源库
。
-
内部库
是指该库和主项目共享一个Repo
,它可以共享到主项目的所有模块中。 -
私有库
是指该库使用独立的私有Repo
,它可以共享到公司多个App
中。 -
开源库
是指该库发布到GitHub
等开源社区提供给其他开发者使用。
这三类库的创建和使用方式都是一致的。在实际操作中,我们一般先创建内部库,如果今后有必要,可以再升级为私有库乃至开源库。下面咱们一起看看怎样创建内部库。
为了方便管理各个内部公共功能组件库,首先我们新建一个叫作Frameworks 的文件夹来保存所有的内部库。这个文件夹和主项目文件夹(在我们例子中是Moments
)以及 Workplace
文档(Moments.xcworkspace
)平衡。例如下面的文件结构:
Frameworks Moments Pods Moments.xcworkspace
然后我们通过CocoaPods
创建和管理这个内部库。
怎么做呢?有两种办法可以完成这项工作,一种是使用pod lib create
[pod name]命令。比如在这个案例当中,我们可以在 Frameworks
文件夹下执行bundle exec pod lib create DesignKit
命令,然后输入邮箱、语言和平台等信息,让 CocoaPods
创建一个 DesignKit.podspec
以及例子项目等一堆文件。具体如下:
DesignKit Example README.md
DesignKit.podspec LICENSE _Pods.xcodeproj
DesignKit.podspec
是 DesignKit
库的 Pod
描述文件,用于描述该 Pod
库的一个特定版本信息。它存放在 CocoaPods
的中心 Repo
供使用者查找
和使用
。
随着这个Pod
库的迭代,CocoaPods
的中心 Repo
会为每个特定的 Pod
版本存放一个对应的 podspec
文件。每个 podspec
文件都包括 Pod
对应Repo
的 URL
、源码存放的位置、所支持的系统平台及其系统最低版本号、Swift
语言版本,以及 Pod
的名字、版本号和描述等信息。
DesignKit
组件库的podspec
文件你可以在仓库中找到。下面是该 podspec
文件的一些
s.name = 'DesignKit'
s.version = '1.0.0'
s.ios.deployment_target = '14.0'
s.swift_versions = '5.3'
s.source_files = 'src/**/*'
s.resources = 'assets/**/*'
name
是该组件的名字,version
是组件的版本号,当我们更新组件的时候同时需要使用 Semantic Versionin
g(语义化版本号)更新该版本号。
ios.deployment_target
为该库所支持的平台和所支持平台的最低版本号。swift_versions
是支持 Swift
语言的版本号。source_files
是该库的源代码所在的文件夹,在我们例子中是 src。resources
是该库资源文件所在的文件夹。
另外一种是手工创建 DesignKit.podspec
文件。我偏向于这一种,因为手工创建出来的项目更简练。
比如在这里,我们只需要在 Frameworks
新建一个叫作 DesignKit
的文件夹,然后在它下面建立 src
和 assets
这两个文件夹,以及 LICENSE
和 DesignKit.podspec
这两个文件即可。
如下所示:
DesignKit.podspec LICENSE assets src
以后所有源代码文件都存放在 src
文件夹下面,而图片、Xib
和 Storyboard
等资源文件存放在 assets
文件夹下。
LICENSE
是许可证文件,如果是开源库,我们必须严格选择一个许可证,这样才能方便其他开发者使用我们的库。
检测内部公共功能组件库
为了保证组件库的使用者能顺利安装和使用我们的库,当我们配置好 DesignKit.podspec
文件后,需要执行bundle exec pod spec lint
命令来检测该podspec
文件是否正确。如果我们维护的是一个开源库,这一步尤为重要。因为它会影响到使用者的第一印象,因此我们在发布该Pod
之前需要把每个错误或者警告都修复好。
不过需要注意的是, CocoaPods
对内部库的检测存在一个 Bug
, 会显示下面的警告:
Missing primary key for
source
attribute
由于我们创建的是内部库,所以可以忽略这个警告,只要没有其他错误信息就可以了。
使用内部公共功能组件库
使用内部公共功能组件库非常简单,只要在主项目的 Podfile
里面使用:path
来指定该内部库的路径即可。
pod 'DesignKit', :path => './Frameworks/DesignKit', :inhibit_warnings => false
当执行bundle exec pod install
命令以后,CocoaPods
会在 Pods
项目下建立一个Development Pods
文件夹来存放所有内部库的相关文件。
![](https://img.haomeiwen.com/i2280900/d3a067076c7e91ed.jpg)
有了 CocoaPods
,我们新建、管理和使用公共组件库就会变得非常简单。下面我们介绍下如何开发设计组件 DesignKit
。
DesignKit 设计组件
DesignKit
是一个设计组件,用于封装与UI
相关的公共组件。为了方便维护,每次新增一个组件,我们最好都建立一个独立的文件夹,例如把 Spacing.swift
放在新建的 Spacing
文件夹中。
![](https://img.haomeiwen.com/i2280900/2c81f764241dc9b2.jpg)
下面以几乎每个App
都会使用到的三个组件:间距(Spacing
)、头像(Avatar
)和点赞按钮(Favorite Button
)为例子,介绍下如何封装基础设计组件。
间距
为了呈现信息分组并体现信息的主次关系,所有App
的所有页面都会使用到间距来添加留白效果。
间距看起来这么简单,为什么我们还需要为其独立封装为一个公共组件呢?主要原因有这么几条。
- 可以为整个
App
提供一致的体验,因为我们统一定义了所有间距,各个功能模块的UI
呈现都保持一致。 - 可以减低设计师和开发者的
沟通成本
,不会再为某些像素值的多与少而争论不休。设计师只使用预先定义的间距,而开发者也只使用在代码中定义好的间距就行了。 - 可以减低设计师的工作量,很多
UI
界面可以只提供一个设计稿来同时支持iOS、Android
以及移动Web
。因为设计师只提供预先定义的间距名,而不是hardcoded
(硬编码)的像素值。不同设备上像素值有可能不一样,但间距名却能保持一致。 - 在支持响应式设计的时候,这些间距定义可以根据设备的宽度而自动调整。这远比硬编码的像素值灵活很多,例如在
iPhone
中twoExtraSmall
是4 points
,而在iPad
中是6 points
。
别看间距公共组件有那么多优点,但实现起来并不难,一个struct
就搞定了,简直是一本万利的投入。
public struct Spacing {
public static let twoExtraSmall: CGFloat = 4
public static let extraSmall: CGFloat = 8
public static let small: CGFloat = 12
public static let medium: CGFloat = 18
public static let large: CGFloat = 24
public static let extraLarge: CGFloat = 32
public static let twoExtraLarge: CGFloat = 40
public static let threeExtraLarge: CGFloat = 48
}
有了上述的定义以后,使用这些间距变得很简单。请看:
import DesignKit
private let likesStakeView: UIStackView = configure(.init()) {
$0.spacing = Spacing.twoExtraSmall
$0.directionalLayoutMargins = NSDirectionalEdgeInsets(top: Spacing.twoExtraSmall, leading: Spacing.twoExtraSmall, bottom: Spacing.twoExtraSmall, trailing: Spacing.twoExtraSmall)
}
我们可以先 import
(引入) DesignKit
库,然后通过Spacing
结构体直接访问预定义的间距,例如Spacing.twoExtraSmall
。
头像组件
iOS
开发者都知道,头像组件应用广泛,例如在房产App
中显示中介的头像,在我们例子 Moments App
中显示自己和好友头像,在短视频App
中显示视频博主头像等。
也许你会问,头像那么简单,为什么需要独立封装为一个组件?原因主要是方便以后改变其UI
的呈现方式,例如从圆角方形改成圆形,添加边界线(border
),添加阴影效果(shadow
)等。有了独立的组件以后,我们只需要修改一个地方就能把这个App
的所有头像一次性地修改呈现效果。
下面是头像组件的实现方式:
public extension UIImageView {
func asAvatar(cornerRadius: CGFloat = 4) {
clipsToBounds = true
layer.cornerRadius = cornerRadius
}
}
我们为UIKit
所提供的UIImageView
实现了一个扩展方法asAvatar(cornerRadius:)
,该方法接收cornerRadius
作为参数来配置圆角的角度,默认值是4
。
使用也是非常简单,只有创建一个UIImageView
的实例,然后调用asAvatar(cornerRadius:)
方法即可。
private let userAvatarImageView: UIImageView = configure(.init()) {
$0.asAvatar(cornerRadius: 4)
}
点赞按钮
可以说,每个具有社交属性的App
都会用到点赞功能,所以在开发当中,点赞按钮也是必不可少的功能组件。
那么,点赞按钮该如何封装呢?和人像组件十分类似,我们可以通过扩展UIButton
来实现。示例代码如下:
public extension UIButton {
func asStarFavoriteButton(pointSize: CGFloat = 18, weight: UIImage.SymbolWeight = .semibold, scale: UIImage.SymbolScale = .default, fillColor: UIColor = UIColor(hex: 0xf1c40f)) {
let symbolConfiguration = UIImage.SymbolConfiguration(pointSize: pointSize, weight: weight, scale: scale)
let starImage = UIImage(systemName: "star", withConfiguration: symbolConfiguration)
setImage(starImage, for: .normal)
let starFillImage = UIImage(systemName: "star.fill", withConfiguration: symbolConfiguration)
setImage(starFillImage, for: .selected)
tintColor = fillColor
addTarget(self, action: #selector(touchUpInside), for: .touchUpInside)
}
}
private extension UIButton {
@objc
private func touchUpInside(sender: UIButton) {
isSelected = !isSelected
}
}
其核心逻辑把当前UIButton
对象的普通 (.normal
) 状态和选中 (.selected
) 状态设置不同的图标。比如在这里我就把星星按钮的普通状态设置成了名叫 “Star”
的图标,并把它的选中状态设置成了名叫 “tar.fill"”
的图标。
注意,这些图标来自苹果公司的SF Symbols
不需要额外安装,iOS 14
系统本身就自带了。而且它们的使用也非常灵活,支持字号、字重、填充色等配置。
使用点赞按钮组件也非常简单,只需要建立一个UIButton的实例,然后调用asStarFavoriteButton方法就可以了。
private let favoriteButton: UIButton = configure(.init()) {
$0.asStarFavoriteButton()
}
点赞按钮的运行效果,也可以在内部菜单查看。
以上我们以间距、头像、点赞按钮为例介绍了如何使用DesignKit
封装与UI
相关的公共组件。以我多年的开发经验来说,在封装UI
组件的时候,可以遵循下面几个原则。
- 尽量使用扩展方法而不是子类来扩展组件,这样做可以使其他开发者在使用这些组件时,仅需要调用扩展方法,而不必使用特定的类。
- 尽量使用代码而不要使用
Xib
或者Storyboard
,因为有些App
完全不使用Interface Builder
。 - 如果可以,要为组件加上
@IBDesignable
和@IBInspectable
支持,这样能使得开发者在使用Interface Builder
的时候预览我们的组件。 - 尽量只使用
UIkit
而不要依赖任何第三方库,否则我们可能会引入一个不可控的依赖库。
总结
前面我介绍了如何封装公共功能组件库,以及以怎样封装基础设计组件,希望对你有所帮助。合理使用功能组件可以让你的开发事半功倍。
![](https://img.haomeiwen.com/i2280900/0cfc2d7877e5273f.jpg)
不过,在封装组件的时候,还需要提醒你注意这么几点。
首先
,为了减低组件之间的耦合性,提高组件的健壮性,组件的设计需要符合单一功能
原则 。也就是说,一个组件只做一件事情
,一个组件库只做一类相关的事情。每个组件库都要相对独立且功能单一。
比如,我们可以分别封装网络库、UI
库、蓝牙处理库等底层库,但不能把所有库合并在一个单独的库里面,这样可以方便上层应用按需使用这些依赖库。例如,广告 SDK
可以依赖于网络库、UI
库,但并不依赖蓝牙处理库。这样做一方面可以减少循环依赖的可能性,另一方面可以加快编译和链接的速度,方便使用。
其次
,每次发布新增和更新组件的时候,都需要严格按照 Semantic Versioning
来更新版本号,这样有效防止因为版本的问题而引入Bug
。
最后
,组件的开发并不是一蹴而就,很多时候可以根据业务需求把公共模块一点点地移入公共组件库中,一步步地完善组件库的功能。不要为了开发组件而开发组件,很多时候当我们充分理解了使用者的需求后,才能为组件定义完善的接口和完整的功能。
源码地址:
DesignKit 源代码:https://github.com/lagoueduCol/iOS-linyongjian/tree/main/Frameworks/DesignKit
网友评论