写在前面的话:在Swift中除了class和函数是引用类型,其它基本都是值类型;而在SwiftUI中更是将值类型用到极致,比如一个View都可以用一个Struct来创建,swift推荐大家能用Struct就用,尽量少用Class,因为Class的复杂程度很高。
1. ForEach
ForEach一般用在生成一组some View.
1.1 Identifiable
ForEach接受一个数组,且数组中的元素必须需要满足 Identifiable 协议,就是数组中的元素有唯一标识符,可以被区分。
struct Item:Identifiable{
var id = UUID()
var message: String
}
struct ContentView: View {
let items = [Item(message: "hello"), Item(message: "world")]
var body: some View {
VStack {
ForEach(items) {
Text($0.message)
}
}
}
}
1.2.Hashable
但是数组中的每一个元素都不遵循 Identifiable,我们可以使用 ForEach(_:id:) 来通过某个支持 Hashable 的 keyPath 来获取一个等效的元素的 Identifiable 的数组,其实就是用hash值来区分的。
struct Item {
var message: String
}
struct ContentView: View {
let items = [Item(message: "hello"), Item(message: "world")]
var body: some View {
VStack {
// 字符串,整数,浮点和布尔值,还有事件集合(even sets)都遵循Hashable。
其他类型(例如,选项(optionals),数组(Array)和范围(Range))在其类型参数实现符合Hashable时就会自动变为hashable。
ForEach(items,id: \.message) {
Text($0.message)
}
}
}
}
或者直接让数组中的元素遵循Hashable。
struct Item: Hashable {
var message: String
}
struct ContentView: View {
let items = [Item(message: "hello"), Item(message: "world")]
var body: some View {
VStack {
ForEach(items,id: \.self) {
Text($0.message)
}
}
}
}
2. NavigationView 和 toolBar
NavigationView就类似于UIKit中的UINavigationController,而toolBar就是导航条上面的按钮元素(NavigationItem)
NavigationLink就是处理每一个cell的点击事件。
struct Item: Hashable {
var message: String
}
struct ContentView: View {
let items = [Item(message: "hello"), Item(message: "world")]
var body: some View {
NavigationView {
List(items, id: \.self) { item in
ZStack {
NavigationLink { // 这个NavigationLink里面的视图不会刷新
Text("click cell")
} label: {
EmptyView() // 为了去掉NavigtionLink右边的箭头
}.opacity(0)
Text(item.message)
}
}.listStyle(.plain)
.navigationTitle("Jonathan")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Image(systemName: "plus.circle")
}
ToolbarItemGroup(placement: .navigationBarTrailing) {
Image(systemName: "plus.circle")
Image(systemName: "plus.circle")
}
}
}
}
}
注意:
如果想自定义左上角返回的按钮,则先要用.navigationBarBackButtonHidden(true)修饰符去隐藏掉默认的放回按钮,然后用ToolBarItem去加。
如果想要左滑删除等操作,需要在List中嵌套一层ForEach.
当然也可以用List中的每一个row的.swipActions修饰符来自定义左滑或者右滑按钮。
List {
ForEach(items) { item in
// do something
}.onDelete { indexSet in
// 做删除操作
}
}
3. TabView 和 TabItem
TabView就类似于UIKit中UITabBarViewController,而TabItem就是TabView上面的一个元素。
var body: some View {
TabView {
NavigationView {
List(items, id: \.self) { item in
Text(item.message)
}.listStyle(.plain)
.navigationTitle("Jonathan")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Image(systemName: "plus.circle")
}
ToolbarItemGroup(placement: .navigationBarTrailing) {
Image(systemName: "plus.circle")
Image(systemName: "plus.circle")
}
}
}.navigationViewStyle(.stack) // 设置图片和文本的排放方式
.tabItem {
Image(systemName: "message.fill")
Text("Message")
}
Text("Second Page").tabItem {
Image(systemName: "person.2.fill")
Text("User")
}
}.onAppear {
// 设置navigationBar模糊的效果
let navigationAppearance = UINavigationBarAppearance()
navigationAppearance.configureWithDefaultBackground()
UINavigationBar.appearance().standardAppearance = navigationAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navigationAppearance
// 设置tabBar的模糊效果
let tabBarAppearance = UITabBarAppearance()
tabBarAppearance.configureWithDefaultBackground()
// 设置tabbarItem的颜色
let tabBarItemAppearance = UITabBarItemAppearance()
// tabBarItemAppearance.normal.iconColor 设置为选中情况下的颜色
tabBarItemAppearance.selected.iconColor = UIColor.green
tabBarItemAppearance.selected.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: UIColor.green
]
tabBarAppearance.stackedLayoutAppearance = tabBarItemAppearance
UITabBar.appearance().standardAppearance = tabBarAppearance
UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance
}
}
4. 属性包装器 propertyWrapper
4.1 UIApplicationDelegateAdaptor
将UIApplicationDelegate的方法包装到SwiftUI.
import SwiftUI
@main
struct JonathanApp: App {
//就是利用AppDelegate实例化了一个UIApplicationDelegateAdaptor 并给变量appDelegate
//类似这行代码:private var appDelegate = UIApplicationDelegateAdaptor(AppDelegate.self)
@UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
// Do something
// 用户在上下拖动页面的时候 键盘会消失
UIScrollView.appearance().keyboardDismissMode = .onDrag
return true
}
}
4.2 Environment中取scenePhase
scenePhase类似于UIKit中的SceneDelegate.
@Environment(\.scenePhase) var scenePhase
var body: some Scene {
WindowGroup {
ContentView()
}.onChange(of: scenePhase) { newScenePhase in
switch newScenePhase {
case .background:
print("App entries background")
case .inactive:
print("App is inactive")
case .active:
print("App is active")
@unknown default:
break
}
}
}
4.3 State(绑定数据到视图)
在Struct中,想要修改一个属性,则必须通过一个mutating的方法来进行修改;
而用State修饰的属性,则可以直接修改;
而且当state修饰的属性值改变的时候,将会自动刷新改属性相关联的视图。类似双向绑定。
@State private var isOn = false
var body: some View {
Button {
isOn.toggle() // 当isOn为true 执行toggle之后 isOn就为false了
} label: {
Text(isOn ? "on" : "off")
}
}
原理,当被state修饰的属性改变的时候,这个属性会被copy一份,进行修改,改完之后把新的重新赋给这个属性,然后触发didSet方法,进一步触发视图的刷新。
4.4 Binding
Binding用于绑定两个属性,如果父视图中的这个属性的value改变的时候,也会同事影响到子视图的这个属性,就是子视图的这个属性也会同时改变;相反,如果子视图的这个属性的value发生改变,其父视图这个属性的value也会发生改变;
// 绑定连个view的isOn属性
struct StateView: View {
@State var isOn = false
var body: some View {
ZStack {
ZStack(alignment: .bottomLeading) {
Rectangle().frame(height: 500)
Button {
isOn.toggle()
} label: {
Text(isOn ? "on" : "off")
}
}
SubStateView(isOn: $isOn) // 表示传的是isOn的引用
}
}
}
struct SubStateView: View {
// single source of truth 单一数据源原则
@Binding var isOn: Bool // 且这里不能给初始值 而这个类型是 Binding<Bool>
var body: some View {
Text(isOn ? "Open" : "Close")
}
}
在SwiftUI中初始化好多控件的时候,都会用到Binding类型的参数,不如Toggle,TextField,Picker等。
struct TextFieldView: View {
@State var content: String = ""
var body: some View {
TextField("please input", text: $content)
}
}
4.5 FocusState
用于设置光标是否在textFiled内部;
struct TextFieldView: View {
@State var content: String = ""
@State var content2: String = ""
enum Field {
case content, content2
}
@FocusState private var isFocused: Field? // 目前无初始值
var body: some View {
Form {
Section {
TextField("please input content", text: $content).focused($isFocused, equals: .content)
TextField("please input content2", text: $content2).keyboardType(.default).focused($isFocused, equals: .content2)
}
Section {
Button("Done") {
// do something
if content.isEmpty {
isFocused = .content
}else {
// do something
}
}
// .disabled(true)
}
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Doen") {
isFocused = nil
}
}
}
.onAppear {
// 延迟为了让页面完全加载
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
isFocused = .content
}
}
}
}
4.6 ViewBuilder
当一个属性被ViewBuilder修饰的时候,则这个属性里面可以放多个视图,也可以进行一些逻辑判断,比如body.
通常属性、闭包都可以被@ViewBuilder修饰。
4.7 Publised,StateObject,ObservedObject
详情见后文:11.MVVM
4.8 EnviromentObject
一般用于夸页面传值,EnvironmentObject 就像把一个对象全局化了一样。
View A -> View B -> View C,现在在C中访问A的对象;
View A中:
用.environmentObject(thing)封装对象,注意调用在Body的根View.
var thing: Thing = Thing(tag: "e")
var body: some View {
NavigationView {
ViewB()
}.environmentObject(thing)
}
View C中:
用@EnvironmentObject var thing: Thing直接访问即可。
@EnvironmentObject var thing: Thing
var body: some View {
// thing.tag: "e"
Text(thing.tag)
}
5. Picker控件
struct PickerView: View {
@State var content = 0
let array = ["Seg one","Seg two","Seg three"]
var body: some View {
Picker("Select", selection: $content) {
ForEach(0..<array.count, id: \.self) { index in
// 下面的代码类似这行:Text(array[index]).tag(index),因为ForEach默认会将id赋给tag
Text(array[index])
}
}.pickerStyle(.segmented)
}
}
6. alert修饰符
struct AlertView: View {
@State var alertValue: Bool = false
var body: some View {
Form {
Section {
Button("Done") {
alertValue = true
}
}
}
.alert("Alert", isPresented: $alertValue, actions: {
Button("Done", role: .none) {
// do something
}
}, message: {
Text("This is message!")
})
}
}
7. group
类似stack,但是没有排序规则,用于包裹一组视图;
group里面最多放10个静态视图;
8. 常用动画
8.1 animation动画(显式和隐式)
注意给视图设置animation动画的时候,该视图必须已经加载到页面上。
struct AnimationView: View {
@State private var isLike = false
var body: some View {
Image(systemName: isLike ? "heart.fill" : "heart")
.foregroundColor(isLike ? .red : .black)
.scaleEffect(isLike ? 1.5 : 1)
//.animation(.default, value: isLike) // 当isLike变化的时候 触发这个动画,
// 所以缺点就是很明显的 只能监听一个值得改变,这称为隐式动画implicate animation
.onTapGesture {
// 弹簧动画:response-表示刚度 dampingFraction-表示阻尼
withAnimation(.spring(response: 0.2, dampingFraction: 0.2)) { // 这个就是显式动画 explicate Animation
isLike.toggle()
}
}
}
}
8.2 stroke & overlay & opacity 修饰符
- stroke 对视图进行勾勒描边;
- overlay直接覆盖视图;
- opacity 修改透明度;
struct AnimationView: View {
@State private var count: CGFloat = 1
var body: some View {
Button("Click Me") {}
.foregroundColor(.red)
.background(.red)
.clipShape(Circle())
.overlay {
Circle().stroke(.red, lineWidth: 10)
.scaleEffect(count)
.opacity(Double(2 - count))
.animation(.easeOut(duration: 2).repeatForever(autoreverses: false), value: count)
}
.onAppear {
count = 2
}
}
}
8.3 transition修饰符
transition 顾名思义,就是移动的动画;
@State private var show = false
var body: some View {
VStack(spacing: 20) {
Button(show ? "Hide" : "Show"){
withAnimation(.linear(duration: 2)) {
show.toggle()
}
}
VStack {
if show {
Image(systemName: "sun")
.foregroundColor(.red)
.transition(.slide.combined(with: .opacity))
}
}
}
}
9. SwiftUI中的Self
Self表示当前类型本身;
@ViewBuilder var body: Self.Body { get }
10. present和dismiss
- 页面的present方式跳转,需要用到一个sheet修饰符;
- 如果想要全屏弹出,则用fullScreenCover修饰符;
@State private var nextPage = false
var body: some View {
TabView {
NavigationView {
List(items, id: \.self) { item in
Text(item.message)
}
}.navigationViewStyle(.stack)
.sheet(isPresented: $nextPage) {
// 放入目的View即可
Text("next page")
}
}
而手动dismiss的时候,需要从Environment中获取dismiss对象.
@Environment(\.dismiss) var dismiss
然后调用dismiss()即可关闭page. 注意直接在对象后面加(), 表示直接调用了这个对象的callAsFunction()方法。
而这个dismiss方法可以关闭push出来的页面,也可以关闭present出来的页面。
11. MVVM设计模式在SwiftUI中的使用
一般在ViewModel中做一些逻辑处理,比如取数据。而ViewModel一般是个Class. 而且要用@Published (表示这个数据被公开) 来修饰从服务器获取到的数据模型,且这个class必须遵循ObservableObjct (表示可以被监听)协议;
@Published 表示向外发布,其他地方有订阅(检测、观察)他的变化,一旦变化,则刷新相关视图;
class ViewModel: ObservableObject {
@Published var modelContent = [Item(message: "one"), Item("two")]
}
// 注意在ViewModel中可以主动调用 objectWillChange.send() 来刷新视图。
在View中用对象包装器StateObject (作用类似State包装器,第一次创建ViewModel的时候,当刷新当前视图的时候,这个被StateObject修饰的对象是不会被刷新的)修饰ViewModel对象。
@StateObject var viewModel = ViewModel()
而在其它用到ViewModel数据的Page,用ObservedObject (作用类似binding包装器,表示监听这个ViewModel对象,当刷新当前视图的时候,这个被ObservedObject修饰的对象是会被刷新的) 来修饰ViewModel.
@ObservedObject var viewModel: ViewModel
当然:如果一个ViewModel如果要一层一层被传递好几个页面的时候,我们可以在viewModel的初始化页面(或者直接将@StateObject放到App page,岂不痛快,然后在其它page都可以取到这个viewModel)
使用.environmentObject(viewModel) 修饰符(注意这个修饰符是在body的根view上的),然后直接在它的子页面用@EnvironmentObject var viewModel: ViewModel 获得这个viewModel,而就不用使用@ObservedObject一层层的传值了。
12. 小零碎
12.1 获得屏幕的宽高(GeometryReader)
GeometryReader {
Rectangle().frame(width: $0.size.width)
}
12.2 数据转模型
首先让Struct模型遵循Codable协议;
然后 JSONDecoder.decode([Model].self, from: data) 搞定。
12.3 自定义修饰符
// 自定义个修饰符 将font和padding放在一个修饰符里面
struct BoldTitleWithBottomPadding: ViewModifier {
func body(content: Content) -> some View {
content.font(.title.bold())
.padding(.bottom, 5)
}
}
extension View {
func boldTitleWithBottomPadding() -> some View {
modifier(BoldTitleWithBottomPadding())
}
}
// 调用
Text("Jonathan").boldTitleWithBottomPadding()
12.4 数据存储
数据存储的方式有UserDefaults(轻量的数据存储)、@AppStorage、CoreData.
@AppStorage这个属性包装器是会把简单数据存放到UserDefaults.
@AppStorage("key") var value = ""
// 这句话 结合了@State var value = ”“的定义,而且当value的值改变的时候,会自动将其存到UserDefault.
CoreData是封装了操作SQLite的方法,是其对象化,用对象的方式去操作数据库。
13. Concurrency
- Swift5.5 的concurrency是专门来处理多线程的;
- 首先要说的是 await 必须与 async 搭配使用;
- 使用Task的原因是在同步线程和异步线程之间,我们需要一个桥接,我们需要告诉系统开辟一个异步环境;
0. 包装closure成一个 await - async
func verifyData(id: String) async throws -> Bool {
try await withCheckedThrowingContinuation { continuation in
self.verify(by: id) { result in
switch result {
case let .success(isSucceed): continuation.resume(returning: isSucceed)
case let .failure(err): continuation.resume(throwing: err)
}
}
}
}
// 使用
func test() {
Task {
do {
let isSucceed: Bool = try await verifyData(id)
} catch {// do something}
}
}
1.单个异步
func downloadImage(url: String) async -> UIImage? {
return await loadImage(url: url)
}
// 使用: 不用再function之后加 async, 在Task中执行完耗时操作之后,需要回到主线程,也可用@MainActor修饰。
func test() {
Task {
let image = await self.downloadImage(
DispatchQueue.main.async {
// do something
}
}
}
2. 并发下载多张图片
func multiImageLoad() async -> [UIImage]? {
var results: [UIImage] = []
for url in urls.enumerated() {
results.append(await self.loadImage(url: url)!)
}
return results
}
// 使用:
Task {
guard let images = await multiImageLoad() else { return }
DispatchQueue.main.async {
// do something
}
}