SwiftUI 教程 1.1 文本与图片
Tags: Swift, Tutorial, Swiftui, Apps, iOS

Eul

本文为 Eul 样章，如果您喜欢，请移步 AppStore/Eul 查看更多内容。 Eul 是一款 SwiftUI 教程类 App（iOS、macOS），以文章（文字、图片、代码）配合真机示例（Xcode 12+、iOS 14+，macOS 11+）的形式呈现给读者。笔者意在尽可能使用简洁明了的语言阐述 SwiftUI 相关的知识，使读者能快速掌握并在 iOS 开发中实践。

Text

本地化字符串

SwiftUI 中涉及到字符串的地方，基本都支持普通的字符串和本地化字符串。Text 的初始化方法也不例外：

/// 普通字符串

init<S>(_ content: S) where S : StringProtocol /// 本地化字符串

init(_ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil)

我们先创建多语言文件，分别写入中英文的 Stay Hungry, Stay Foolish! 文本，通过枚举去获取对应的 LocalizedStringKey，然后就可以使用 Text(LocalizeKey.Hungry) 方便地展示本地化字符串了。

enum LocalizeKey {

static let kHungry: LocalizedStringKey = "Hungry"

} struct LocalizableView: View {

var body: some View {

Text(LocalizeKey.kHungry)

}

} // "Hungry" = "Stay Hungry, Stay Foolish!";

// "Hungry" = "求知若饥，虚心若愚!";

富文本

Text 实现了操作符重载，我们可以直接用 + 来拼接不同样式的文字。

struct RichTextView: View {

private let text: Text =

Text("Stay ").foregroundColor(.blue).font(.title).italic() +

Text("Hungry, ").font(.headline) +

Text("Stay ").foregroundColor(.red).font(.title) +

Text("Foolish!").font(.headline).underline()



var body: some View {

text

}

}

另外，Text 本身遵循 Equatable 协议，我们还可以直接使用 == 和 != 来对两个 Text 进行判等。

日期

Text 甚至可以直接展示日期，现在创建一个倒计时控件只需要一行代码就可以实现！

Text 的初始化方法有如下几种：

/** 以下日期均指当地日期 */ /// 使用指定样式展示日期

public init(_ date: Date, style: Text.DateStyle) /// 展示日期范围

public init(_ dates: ClosedRange<Date>) /// 展示日期间隔

public init(_ interval: DateInterval)

DateStyle 有如下枚举值：

public struct DateStyle {

/// 时间，比如：11:23PM

public static let time: Text.DateStyle



/// 日期，比如：June 3, 2019

public static let date: Text.DateStyle



/// 相对现在的时间，比如：2 hours, 23 minutes

public static let relative: Text.DateStyle



/// 与现在的时间差，比如：-3 months，+2 hours

public static let offset: Text.DateStyle



/// 倒计时，比如：36:59:01

public static let timer: Text.DateStyle

}

下面我们通过代码展示其用法：

struct DateView: View {

private var future: Date { now.addingTimeInterval(3600) }

private var now: Date { Date() }



var body: some View {

VStack(alignment: .leading, spacing: 10) {

row(style: ".date") { Text(now, style: .date) }

row(style: ".offset") { Text(future, style: .offset) }

row(style: ".relative") { Text(future, style: .relative) }

row(style: ".time") { Text(future, style: .time) }

row(style: ".timer") { Text(future, style: .timer) }

row(style: "Range") { Text(now...future) }

row(style: "Interval") { Text(DateInterval(start: now, end: future)) }

}

}



func row<Content: View>(style: String, @ViewBuilder content: () -> Content) -> some View {

VStack {

HStack {

content()

Spacer()

Text(style).foregroundColor(.secondary)

}



Divider ()

}

}

}

先简述一下 @ViewBuilder 的作用：它可以用来修饰闭包参数，并从中构建视图。

.offset 、 .relative 和 .timer 展示的时间都是根据秒数变化的，其它样式的日期则是静态的。

Label

构建方法

Label 是一个相当强大的控件，可以快速生成图片和文字的组合，默认布局是左图右文，也支持自定义配置。

它有如下初始化方法：

init<S>(S, image: String) init<S>(S, systemImage: String) init(LocalizedStringKey, image: String) init(LocalizedStringKey, systemImage: String) // Title: View, icon: View

init(title: () -> Title, icon: () -> Icon)

我们试着用以上方法构建不同的视图，代码和界面如下：

Label("Swift", systemImage: "swift")

.foregroundColor(.orange) Label(

title: {

Text("Apple")

},icon: {

Image(systemName: "applelogo")

}

)

.foregroundColor(.blue) Label(

title: {

Image(systemName: "gift.fill")

.renderingMode(.original)

},icon: {

Text("Gift")

}

)

.foregroundColor(.red)

.labelStyle(TitleOnlyLabelStyle())

LabelStyle 有如下三种样式：

DefaultLabelStyle // Title + Icon

IconOnlyLabelStyle // 只显示 Icon

TitleOnlyLabelStyle // 只显示 Title

自定义样式

上面的构建方法中，其实还有一种是未曾提及的：

init(LabelStyleConfiguration)

LabelStyleConfiguration 是一个结构体类型，包含 Icon 和 Title。

我们可以通过这个初始化方法，给系统提供的样式添加自定义的样式。

比如我们需要给 Label 加上阴影，可以先创建一个遵循 LabelStyle 协议的 ShadowLabelStyle，然后使用该样式。下面代码中的 Configuration 实际上就是 LabelStyleConfiguration ，只不过系统通过 typealias Configuration = LabelStyleConfiguration 改头换面了而已。

Label("Apple", systemImage: "applelogo")

.labelStyle(ShadowLabelStyle()) struct ShadowLabelStyle: LabelStyle {

func makeBody(configuration: Configuration) -> some View {

Label(configuration)

.shadow(color: Color.black.opacity(0.5), radius: 5, x: 0, y: 5)

}

}

上面的样式有一定的局限性，如果我们需要一个垂直布局或是左右对齐的样式呢？实现的原理是一样的，代码如下：

struct VerticalLabelStyle: LabelStyle {

func makeBody(configuration: Configuration) -> some View {

VStack(alignment: .center, spacing: 10) {

configuration.icon

configuration.title

}

}

} struct LeftRightLabelStyle: LabelStyle {

func makeBody(configuration: Configuration) -> some View {

HStack(alignment: .center, spacing: 10) {

configuration.icon

Spacer()

configuration.title

}

}

}

TextField

TextField 有如下的三种构建方式：

普通的初始化方法 在方法 1 的基础上新增了监听功能，可以监听编辑状态、Return 键的按下动作 在方法 2 的基础上新增了格式转换功能

前两种方法比较简单，这里说一下方法 3 的细节。如下样例是将输入的文字转换成数字，当我们正在输入的时候，格式转换功能是不生效的，只有当编辑结束的时候，才会去执行转换，如果转换成功，会更新绑定的值（s3），如果转换失败，不会更新 s3。

@State private var s1 = ""

@State private var s2 = ""

@State private var s3 = 0

@State private var pwd = "" GroupBox(label: Text(s1)) {

/// 1

TextField("TextField", text: $s1)

} GroupBox(label: Text(s2)) {

/// 2

TextField("Observe TextField", text: $s2) { (isEditing) in

print(isEditing)

} onCommit: {

print("Return")

}

.textFieldStyle(RoundedBorderTextFieldStyle())

} GroupBox(label: Text(String(s3))) {

/// 3

TextField("Formatter TextField", value: $s3, formatter: NumberFormatter()) {

(isEditing) in

print(isEditing)

} onCommit: {

print("Return")

}

.textFieldStyle(RoundedBorderTextFieldStyle())

.keyboardType(.numbersAndPunctuation)

} GroupBox(label: Text("密码输入: \(pwd)")) {

/// 密码输入

SecureField("Password", text: $pwd)

}

TextEditor

TextEditor 的使用比较简单，如下代码我们就可以轻松创建一个文本输入框：

@State private var text = "Stay Hungry, Stay Foolish!" TextEditor(text: $text)

.frame(height: 150)

.lineSpacing(10.0) // 行距

.multilineTextAlignment(.center) // 对齐方式

.overlay(

RoundedRectangle(cornerRadius: 5)

.stroke(Color.blue, lineWidth: 1)

)

系统并没有提供 placeholder 这样的特性，不过我们可以轻松实现这个功能。

// 添加 placeholder

ZStack(alignment: .topLeading) {

TextEditor(text: $text2)

.frame(height: 150)

.border(Color.blue)



if text2.isEmpty {

Text("Type something")

.foregroundColor(Color(UIColor.placeholderText))

.padding(8)

}

}

Image

Image 用来展示图片，它可以加载资源包中的图片文件和系统内置的图标（SF Symbols）。

以下是几点提示：

图片默认具有伸展特性， .resizable() 可使图片不超出屏幕或指定区域

可使图片不超出屏幕或指定区域 如果要展示 SF Symbols 内置图标自带的颜色，可以用 .renderingMode(.original). 来渲染。

来渲染。 Text 可以直接插入图片，支持字符串插值和拼接两种方式