本篇文章将作为SwiftUI系列文章的开始,也是记录自己SwiftUI的学习之路。理解有问题的地方欢迎大家积极指出来,一起成长。
声明式UI与传统的命令式UI,以下图为例。
命令式UI:第一步创建一个Label,第二步设置他的的坐标,宽高,第三步将Label添加到父视图上。第四步创建一个Button,第五步设置他的坐标,第六步给Button添加绑定事件,第七步将Button添加到父视图上,第八步实现Button绑定的方法,在方法里实现每次点击时将Label显示的数字加一,然后将得到的新的数字赋值给Label。
声明式UI:需要一个页面,这个页面需要一个Label,一个Button。Label的显示内容为一个数字,点击button时这个数值加一,然后Label里的内容就会自动刷新。
命令式UI需要我们告诉计算机一步步该如何做,而声明式UI只需要我们具体描述一个我们想要的页面即可,剩下的工作交给计算机。
对于一种新的布局思想,学习的过程中还是尽量避免用UIKit里的知识去理解SwiftUI里的组件,可能会让我们学的比较纠结。有个不是很恰当的比喻,就像驾校的教练他们最喜欢的就是那种小白,最讨厌的就是那种不知道从哪里学了一点的半吊子,小白最大的优势就是知识干净,没有各种各样的坏习惯,对于新知识的理解比较纯粹,你说什么他就认为是什么。而半吊子则与之相反。在学习新知识的过程中产生的困惑,往往是因为和以前的某个知识产生了碰撞,颠覆了认知,这种碰撞某种程度来说也是件好事,可以让我们对知识有个更深入的认识。
struct ContentView: View {
var body: some View {
Text("hello world")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
可以看到SwiftUI里视图不在是Class,而是用的Struct,这样做最大的好处就是足够轻。像之前我们使用的那些继承UIView的控件,就会比较沉重,查看UIView的定义,里面定义了很多很少用到的属性和方法,但是继承于它的子类不管我们是否需要都会全盘继承,资源开销相对于Struct要大很多。其中的View不能把它理解成UIView,虽然看着很像,但它并不是个组件,它只是一个协议,定义如下。
public protocol View {
associatedtype Body : View
/// The content and behavior of the view.
@ViewBuilder var body: Self.Body { get }
}
基础组件
常用的基础组件有Text、Image、Button、VStack、HStack、ZStack、List、NavigationView、Spacer等等。
样式代码以 Text为例, 示例代码如下
struct ContentView: View {
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text("hello world hello world hello world")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.blue)
.lineLimit(2)
.padding(.all, 10)
.background(
Capsule()
.fill(LinearGradient(gradient: Gradient(colors: [Color.red, Color.orange]), startPoint: /*@START_MENU_TOKEN@*/.leading/*@END_MENU_TOKEN@*/, endPoint: /*@START_MENU_TOKEN@*/.trailing/*@END_MENU_TOKEN@*/))
.foregroundColor(.red).opacity(0.5)
)
.frame(width: 200, height: 150, alignment: .center)
.lineSpacing(10.0)
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.red)
)
.shadow(color: Color.black.opacity(0.15), radius: 20, x: 0, y: 0)
.onTapGesture(count: 1, perform: {
print("点击了")
})
}
}
}
显示效果
解释一下每个方法的作用
font
用来设置字体,默认给我们提供了例如largeTitle、title、title2、title3、headline、subheadline等多种预置字体,使用这些字体的好处是自动适配系统的动态字体大小,用户在设置中调整了字体大小,不需要适配就可以自动调整。
如果这些字体满足不了设计师的想法还可以自定义字体,定义一个大小为16,weight为medium的font。
.font(Font.system(size: 16, weight: .medium, design: .default))
如果系统提供的字体扔无法满足,还可以自定义字体
public static func custom(_ name: String, size: CGFloat) -> Font
fontWeight
用来设置Weight,系统默认提供了一些如ultraLight、thin、light、regular、medium、semibold、bold、heavy等。
foregroundColor
.foregroundColor(Color.blue)
前景颜色,可以用来设置文字内容的显示颜色。传了一个参数Color.blue,看着像是一种颜色,其实它也是View
extension Color : View {
public typealias Body = Never
}
所以有时候想设置背景色的时候可以直接使用Color就行,例如
struct ContentView: View {
var body: some View {
Color.blue
}
}
系统提供了很多颜色可直接使用,例如.clear、.red、.blue等等
显示效果
accentColor
强调色,系统的强调色默认是蓝色,有些控件使用强调色作为默认颜色,比如Button
Button(action: {}) {
Text("Accented Button")
}
这个时候如果想调整文本的颜色有两种方式,一种是设置前景色,另一种是设置强调色。以下两种都可以将内容设置为红色
Button(action: {}) {
Text("Accented Button")
}
.foregroundColor(.red)
Button(action: {}) {
Text("Accented Button")
}
.accentColor(.red)
lineLimit
限制行数
padding
内边距,系统提供了很多种选择
.padding(.top, 10) // 上内边距
.padding(.leading, 10) // 左内边距
.padding(.bottom, 10) // 底部内边距
.padding(.trailing, 10) // 右内边距
.padding(.all, 10) // 四边各设置10内边距
.padding(.horizontal, 10) // 水平方向两边内边距各为10
.padding(.vertical, 10) // 竖直方向两边内边距各为10
background
@inlinable public func background<Background>(_ background: Background,
alignment: Alignment = .center) -> some View where Background : View
这是一个泛型函数,占位类型Background它遵循View协议,第一个参数background也是Background,也就是说我们只要传一个遵循View协议的类型即可,第二个参数是对齐方式。可以设置的样式很多,可以设置一个单一颜色又或者渐变色,甚至设置一个Text("1234")也行,只要遵循View协议即可。
struct TestOne: View {
var body: some View {
Text("Hello, World!")
.background(
// Color.red // 红色
// Text("1234")
// 渐变色
LinearGradient(gradient: Gradient(colors: [Color.red, Color.orange]), startPoint: .leading, endPoint: .trailing)
)
}
}
frame
可以设置宽,高,对其方式。
lineSpacing
行间距
overlay
覆盖物,覆盖一层在View上面。可以用它来给View加一个边框。
struct TestOne: View {
var body: some View {
VStack {
Text("Hello, World!")
.overlay(
// cornerRadius 设为0 则边框是一个矩形
RoundedRectangle(cornerRadius: 0, style: .continuous)
.stroke(Color.red, lineWidth: 1)
)
.padding(.all, 10)
Text("Hello, World!")
.overlay(
// cornerRadius 设为10 则边框是一个椭圆
RoundedRectangle(cornerRadius: 10, style: .continuous)
.stroke(Color.red, lineWidth: 1)
)
.padding(.all, 10)
}
}
}
shadow
用来设置四周的阴影。需要传三个参数,color,radius半径以及x,y偏移量。
@inlinable public func shadow(color: Color = Color(.sRGBLinear, white: 0, opacity: 0.33), radius: CGFloat,
x: CGFloat = 0, y: CGFloat = 0) -> some View
.shadow(color: Color.black.opacity(0.7), radius: 20, x: 0, y: 0)
onTapGesture
点击手势,count为手势触发的连续点击的次数,2也就是连续点击两次触发,perform对应点击事件的处理逻辑。
struct ContentView: View {
var body: some View {
Text("hello world")
.onTapGesture(count: 2, perform: {
print("点击了")
})
}
}
previewLayout 用来设置预览图的大小
ContentView()
.previewLayout(.fixed(width: 300, height: 300)) // 预览图的大小就是300 * 300
如何封装一个组件
遵循三条原则 1.样式与内容分离 ,2.级联性 ,3.通用性
比如实现一个简单的分享面板
struct SXControlButton: View {
var text: String
var backgroundColor: Color
var textColor: Color
var action: () -> ()
var body: some View {
ZStack {
Circle()
.fill(backgroundColor)
Text(text)
.foregroundColor(textColor)
}
.onTapGesture(perform: action)
.frame(width: 60, height: 60)
}
}
struct Zujian_Previews: PreviewProvider {
static var previews: some View {
HStack {
SXControlButton(text: "微信", backgroundColor: .red, textColor: .white) {}
SXControlButton(text: "QQ", backgroundColor: .green, textColor: .white) {}
SXControlButton(text: "微博", backgroundColor: .purple, textColor: .white) {}
}
.previewLayout(.fixed(width: 300, height: 100))
}
}
平时使用UIKit的时候经常会看到这样的代码。自定义一个Button,定义内容text,背景色backgroundColor,内容颜色textColor。这样的代码看着没问题,但扩展性不是很好,比如说现在要加一个高亮色, var lightColor: Color,加了这个属性之后所有使用这个Button的地方的构造方法都要改,除非在定义lightColor的时候给它一个默认值,var lightColor: Color = .red。理想的做法应该是将样式和内容分离,就像CSS和HTML。
可以利用级联性对上面的代码进行优化,从上到下从外到内,在最外层设置foregroundColor和accentColor,则里面的所有视图都会起效,这样做减少了代码量,所有子视图可以共享父视图的样式,并且还可以单独配置自己的风格,可以设置一组样式进行共享。
struct SXControlButton: View {
var text: String
var action: () -> ()
var body: some View {
ZStack {
Circle()
Text(text)
.foregroundColor(.accentColor)
}
.onTapGesture(perform: action)
.frame(width: 60, height: 60)
}
}
struct Zujian_Previews: PreviewProvider {
static var previews: some View {
HStack {
SXControlButton(text: "微信") {}
SXControlButton(text: "QQ") {}
.foregroundColor(.green)
.accentColor(.white)
SXControlButton(text: "微博") {}
.foregroundColor(.purple)
.accentColor(.white)
}
.foregroundColor(.red)
.accentColor(.white)
.previewLayout(.fixed(width: 300, height: 100))
}
}
现在还有一点不完美的地方就是Button的frame大小现在我们是写死的,如果想要灵活设置可以将frame设置放在外面
SXControlButton(text: "微信") {}
.frame(width: 60, height: 60)
又或者一种更优雅一点的写法,通过枚举来实现。首先先引入一个新的概念,Environment
Environment
环境,也可以理解成全局的context上下文,SwiftUI自动创建。
1.使用Environment来传递系统范围的设置,例如ContentSizeCategory,LayoutDirection。
2.也可以使用它来添加与当前视图相关的所有ObservableObject。
3.可以注入当前视图特定的值,例如isEnabled,editMode,presentationMode。
4.并且我们还可以给View自定义环境变量方便使用。比如下面这段代码就是第一条的使用场景。
struct XXX : View {
@Environment(\.sizeCategory) var size
var body: some View {
}
}
通过@Environment属性修饰符我们可以读取环境变量sizeCategory的值,当用户在系统设置中改变了字体大小,View将会根据最新的size进行重新绘制。
接下来开始通过第四条自定义环境变量来实现控件大小的调整,具体实现代码如下。 首先定义枚举包含三个case,small、big、large。对应的高度分别为60,70,80,height和width相等。实现EnvironmentKey协议,需要实现静态属性defaultValue,类型为ViewSize?可选类型,默认值为.small。然后扩展EnvironmentValues,自定义环境变量viewSize,实现get和set,key值为ViewSize自己。通过在父视图设置.viewSize(.large),所有的子视图将自动继承父视图的environment,在内部可以通过@Environment(.viewSize) private var viewSize取出环境中变量值。
enum ViewSize: EnvironmentKey {
static var defaultValue: ViewSize? = .small
case small
case big
case large
var height: CGFloat {
switch self {
case .small:
return 60
case .big:
return 70
case .large:
return 80
}
}
var width: CGFloat {
height
}
}
extension EnvironmentValues {
var viewSize: ViewSize? {
get {
self[ViewSize.self]
}
set {
self[ViewSize.self] = newValue
}
}
}
extension View {
func viewSize(_ size: ViewSize?) -> some View {
self.environment(\.viewSize, size)
}
}
struct SXControlButton: View {
var text: String
var action: () -> ()
@Environment(\.viewSize) private var viewSize
var body: some View {
ZStack {
Circle()
Text(text)
.foregroundColor(.accentColor)
}
.onTapGesture(perform: action)
.frame(width: viewSize?.width, height: viewSize?.height)
}
}
struct Zujian_Previews: PreviewProvider {
static var previews: some View {
HStack {
SXControlButton(text: "微信") {}
SXControlButton(text: "QQ") {}
.foregroundColor(.green)
.accentColor(.white)
SXControlButton(text: "微博") {}
.foregroundColor(.purple)
.accentColor(.white)
}
.foregroundColor(.red)
.accentColor(.white)
.viewSize(.large)
}
}
如果个别子视图需要设置自己的大小,可以使用environment来覆盖父视图的environment。比如
SXControlButton(text: "微信") {}
.environment(\.viewSize, .small)
现在这种写法已经没什么问题了,但通用性还是差一点,我们想要一个可以适用于所有视图的效果,可以把ViewSize抽离出来。
import SwiftUI
enum Size: EnvironmentKey {
static var defaultValue: Size? = .small
case small
case big
case large
}
extension EnvironmentValues {
var size: Size? {
get {
self[Size.self]
}
set {
self[Size.self] = newValue
}
}
}
extension View {
func size(_ size: Size?) -> some View {
self.environment(\.size, size)
}
}
然后进行代码调整
fileprivate extension Size {
var height: CGFloat {
switch self {
case .small:
return 60
case .big:
return 70
case .large:
return 80
}
}
var width: CGFloat {
height
}
}
struct CommonButton: View {
var text: String
var action: () -> ()
@Environment(\.size) private var size
var body: some View {
ZStack {
Circle()
Text(text)
.foregroundColor(.accentColor)
}
.onTapGesture(perform: action)
.frame(width: size?.width, height: size?.height)
}
}
struct CommonView: PreviewProvider {
static var previews: some View {
HStack {
CommonButton(text: "微信") {}
CommonButton(text: "QQ") {}
.foregroundColor(.green)
.accentColor(.white)
CommonButton(text: "微博") {}
.foregroundColor(.purple)
.accentColor(.white)
}
.foregroundColor(.red)
.accentColor(.white)
.size(.large)
}
}
这样代码又变得更加灵活,更加容易自定义。如果想要自定义的View支持Size,只需要添加 @Environment(.size) private var size这句代码即可。
下一篇将更新SwiftUI之布局
参考资料
官方文档: developer.apple.com/documentati…
iCloudEnd:www.jianshu.com/p/53d9672c7…
哔哩哔哩:张弓chmod777,这是一位勇敢的独立开发者,他的视频简短精炼很不错。 www.bilibili.com/video/BV1o5…