SwiftUI入门到精通

SwiftUI要求

iOS13.0+

快捷键

control + option + 点击:出现属性编辑器

command + 点击:出现快捷菜单

command + shift + L:Show Library弹窗

布局

VStack- 垂直布局

HStack- 水平布局

Spacer- 间距

Text- 文本

Image- 图片

Divider- 分割线

Group- 组

ScrollView- 滚动视图

Path- 路径

Shaper- 形状

Form、Section- 表单

Color.red- 填充颜色

ForEach- 循环

LinearGradient(线性渐变)、RadialGradient(径向渐变)、AngularGradient(角度渐变)

代码解析

structContentView:View{varbody:someView{Text("Hello, world!").font(.title).foregroundColor(.yellow).bold().italic()}}structContentView_Previews:PreviewProvider{staticvarpreviews:someView{ContentView()}}

ContentView为布局,ContentView_Previews为预览布局

body为计算属性,类型为不透明类型的View,不透明类型使用some修饰

Swift语法,只有一行代码,return可以省略

some修饰,表示不透明类型,总会返回某一个特定的类型,但是不知道是哪一种

可以返回关联类型的协议类型

安全性:不对外暴露具体的返回类型

用来解决SwiftUI繁琐的不确定返回类型问题

使用技巧

可以在右上角+里拖动空间到代码中

使用import导入所需的库

可以新建SwiftUI View

ignoresSafeArea忽略safeArea的边距,用在feame前

布局group组件可增加padding

VStack可添加font、foregroundColor等属性,对所有包含的元素起效

串联属性,每一个点语法属性,返回当前对象

Text("Hello world!").font(.title).foregroundColor(.purple)A modifier returns a view that applies a new behavior or visual change. You can chain multiple modifiers to achieve the effects you need.

使用previewLayout可以定义预览的窗口的大小,也可以使用Group同时预览多个窗口,通用属性可以提取到外面

structLandmarkRow_Previews:PreviewProvider{staticvarpreviews:some View{Group{LandmarkRow(landmark:landmarks[0])LandmarkRow(landmark:landmarks[1])}.previewLayout(.fixed(width:300,height:70))}}

Identifiable:作为唯一标识

遍历需要唯一标识来遍历,如下:

List(landmarks,id:\.id){landmark inNavigationLink(destination:LandmarkDetail()){LandmarkRow(landmark:landmark)}}

如果让列表中元素遵守Identifiable协议,遍历处即可省略id参数,model中需要有名称为id的属性

structLandmark:Hashable,Codable,Identifiable{varid:Intvarname:String

页面跳转使用NavigationLink,destination为要跳转的页面

NavigationLink(destination: LandmarkDetail()) {    LandmarkRow(landmark: landmark)}

使用NavigationView为页面田健导航栏,可设置navigationTitle等

NavigationView{List(landmarks){landmark inNavigationLink(destination:LandmarkDetail()){LandmarkRow(landmark:landmark)}}.navigationTitle("Landmarks")}

预览窗口按钮作用:

第一个按钮:Live Preview和Debug Preview,未打开只能查看页面,不能点击等,打开之后可以点击跳转页面等交互操作

第二个按钮:Preview On Device,连上真机点击之后,预览可以同步在真机上展示

第三个按钮:Inspect Preview,可以打开窗口属性窗口,可以设置预览窗口属性

第四个按钮:Duplicate Preview,可以复制创建多个预览窗口

代码控制预览的机型

structLandmarkList_Previews:PreviewProvider{staticvarpreviews:someView{LandmarkList().previewDevice(PreviewDevice(rawValue:"iPhone SE (2nd generation)"))}}// 多设备同时预览structLandmarkList_Previews:PreviewProvider{staticvarpreviews:someView{ForEach(["iPhone 8","iPhone 12 Pro Max"],id:\.self){deviceNameinLandmarkList().previewDevice(PreviewDevice(rawValue:deviceName)).previewDisplayName(deviceName)}}}

组合静态的View和动态的view到List里,可使用List + ForEach:

List(filteredLandmarks){landmark inNavigationLink(destination:LandmarkDetail(landmark:landmark)){LandmarkRow(landmark:landmark)}}替换为List{Toggle(isOn:$showFavoriteOnly){Text("Favorites only")}ForEach(filteredLandmarks){landmark inNavigationLink(destination:LandmarkDetail(landmark:landmark)){LandmarkRow(landmark:landmark)}}}

如果遍历的对象没有实现Identifiable协议,则需要传id

List{ForEach(modelData.categories.keys.sorted(),id:\.self){key inText(key)}}

ObservableObject协议

当遵守ObservableObject协议的数据更新时,绑定数据的view会自动更新

finalclassModelData:ObservableObject{@Publishedvarlandmarks:[Landmark]=load("landmarkData.json")}

@Published

使用@Published修饰监听对象的属性,表示该对象的属性需要把属性值的改变更新进去

finalclassModelData:ObservableObject{@Publishedvarlandmarks:[Landmark]=load("landmarkData.json")}

@StateObject

使用@StateObject初始化一个监听对象的数据,

使用.environmentObject把数据设置到环境对象里,

在需要的地方去取环境对象@EnvironmentObject var modelData: ModelData进行使用

@mainstructMySwiftUIApp:App{// 定义@StateObjectprivatevarmodelData=ModelData()varbody:someScene{WindowGroup{ContentView()// 设置.environmentObject(modelData)}}}// 取@EnvironmentObjectvarmodelData:ModelData// 使用modelData.landmarks

Bool的toggle()方法:在true和false之前切换

@EnvironmentObject属性用于在下级view中接收传递过来的参数

environmentObject(_:)方法用于向下级传递参数

@Binding:绑定修饰符用于修饰一个值,这个值用来改变另外一个值

绑定:@BindingvarisSet:Bool修改:FavoriteButton(isSet:$modelData.landmarks[landmarkIndex].isFavorite)

定义有状态的字段,使用@State修饰,定义为private,并且赋初始值

@State private var showFavoritesOnly = false

@State:

使用@State属性为应用程序中的数据建立一个真实来源,您可以从多个视图中修改这些数据。SwiftUI管理底层存储并根据值自动更新视图。

使用Path直接绘制,可以当做View来使用

View动画,包括颜色、透明度、旋转、大小和其他属性等,可以使用withAnimation来包裹状态State实现动画

withAnimation{self.showDetail.toggle()}withAnimation(.easeInOut(duration:4)){self.showDetail.toggle()}

调用View的transition可以为View添加动画

HikeDetail(hike: hike).transition(.slide)

自定义transition可自定义动画

extensionAnyTransition{staticvarmoveAndFade:AnyTransition{AnyTransition.slide}}extensionAnyTransition{staticvarmoveAndFade:AnyTransition{AnyTransition.move(edge:.trailing)}}extensionAnyTransition{staticvarmoveAndFade:AnyTransition{letinsertion=AnyTransition.move(edge:.trailing).combined(with:.opacity)letremoval=AnyTransition.scale.combined(with:.opacity)return.asymmetric(insertion:insertion,removal:removal)}}

ListRowInsets用来调整一个view在list中的上下左右间距

CaseIterable,用在enum中用来获取allCases方法

@Environment,SwiftUI提供了在环境中值的存储,使用@Environment可以访问值,并可以读写

@Environment(\.editMode)vareditMode

@State、@ObservableObject、@Binding、@EnvironmentObject区别

@State和@ObservableObject之间有一些细微的差异。这些都是很重要的,因为它们都有不同的用途。

@State在视图本地。值或数据在视图中本地保存。它由框架管理,由于它存储在本地,因此它是一个值类型。

使用@State来存储不断变化的数据。记住,我们所有的SwiftUI视图都是结构体,这意味着没有@State之类的东西就无法更改它们。

@ObservableObject在视图外部,并且不存储在视图中。它是一种引用类型,因为它不在本地存储,而只是具有对该值的引用。这不是由框架自动管理的,而是开发人员的责任。这最适用于外部数据,例如数据库或由代码管理的模型。

@Binding也在视图内,但是与@State区别在于@Binding用于不通视图之间的参数传递。@Binding和@ObservedObject一样都是传递引用。

@EnvironmentObject可以理解为全局变量

ObservableObject和@Published

遵循ObservableObject协议的类可以使用SwiftUI的@Published属性包装器来自动发布属性的变化,以便使用该类的实例的任何视图能够自动重新调用body属性,保持界面与数据的一致。

@Published var profile = Profile.default

界面中使用@Binding来绑定UI

``

使用UIViewRepresentable来将UIKit中已有的View在SwiftUI中使用

使用UIViewControllerRepresentable来UIKit中的UIViewController在SwiftUI中使用

UIViewRepresentable

使用方法如下:

importSwiftUIimportUIKitstructPageControl:UIViewRepresentable{varnumberOfPages:Int@BindingvarcurrentPage:IntfuncmakeUIView(context:Context)->UIPageControl{letcontrol=UIPageControl()control.numberOfPages=numberOfPagesreturncontrol}funcupdateUIView(_uiView:UIPageControl,context:Context){uiView.currentPage=currentPage}}

SwiftUI画布的Resume快捷键:Option + Command + P

Form表单、Section分段、Group分组

SwiftUI限制Section和Group包含不能超过10个,Section可设置header和footer

Form {            Section(header: Text("Section 1 header").bold(), footer: Text("Section 1 footer")) {                Text("Placeholder 1")                Text("Placeholder 2")                Text("Placeholder 3")                Group() {                    Text("Placeholder 1")                    Text("Placeholder 2")                    Text("Placeholder 3")                    Text("Placeholder 4")                    Text("Placeholder 5")                    Text("Placeholder 6")                    Text("Placeholder 7")                    Text("Placeholder 8")                    Text("Placeholder 9")                    Text("Placeholder 10")                }            }                        Group() {                Text("Placeholder 1")                Text("Placeholder 2")            }        }

WX20210127-160605@2x.png

添加导航栏使用navigationBarTitle,displayMode设置显示样式

NavigationView{Form{Section{Text("Hello World")}}.navigationBarTitle("SwiftUI",displayMode:.inline)}

就像SwiftUI的其他视图一样,VStack最多可以有10个子节点——如果您想添加更多子节点,应该将它们包装在一个Group中。

Color.red本身就是一个视图,这就是为什么它可以像形状和文本一样使用。它会自动占用所有可用空间。

Color.primary是SwiftUI中文本的默认颜色,根据用户的设备是在亮模式还是在暗模式下运行,它将是黑色还是白色。还有Color.secondary,它也可以是黑色或白色,这取决于设备,但现在它有轻微的透明度,以便后面的一点颜色可以穿透。

如果要将内容置于安全区域之下,可以使用edgesIgnoringSafeArea()修饰符指定要运行到的屏幕边缘。

Color.red.ignoresSafeArea()Color.red.edgesIgnoringSafeArea(.all)

渐变色

VStack{// 线性渐变 LinearGradient 沿一个方向运行,因此我们为其提供了一个起点和终点LinearGradient(gradient:Gradient(colors:[.white,.black]),startPoint:.leading,endPoint:.trailing)// 径向渐变 RadialGradient 以圆形向外移动,因此,我们没有指定方向,而是指定了起点和终点半径——颜色应从圆心到圆心的距离开始和停止变化RadialGradient(gradient:Gradient(colors:[.blue,.black]),center:.center,startRadius:20,endRadius:200)// 角度渐变 AngularGradient,尽管您可能听说过其他地方将其称为圆锥形或圆锥形渐变。这使颜色围绕一个圆圈循环而不是向外辐射AngularGradient(gradient:Gradient(colors:[.red,.yellow,.green,.blue,.purple,.red]),center:.center)}

image.png

如果您发现图像已被某种颜色填充,例如显示为纯蓝色而不是实际图片,则可能是SwiftUI为它们着色以显示它们是可点击的。要解决此问题,请使用renderingMode(.original)修饰符强制SwiftUI显示原始图像,而不是重新着色的版本。

Image("Image Name").renderingMode(.original)

Alert的使用

structContentView:View{@StateprivatevarshowAlert=falsevarbody:someView{VStack{Button(action:{showAlert=true}){Text("按钮")}.alert(isPresented:$showAlert,content:{Alert(title:Text("标题"),message:Text("文本内容"),primaryButton:.cancel{print("点击取消")},secondaryButton:.default(Text("确定")){print("点击确定")})})}}}

image.png

Swift内置形状:

矩形Rectangle、圆角矩形RoundedRectangle、圆形Circle、胶囊Capsule和椭圆Ellipse

使用方法:.clipShape(Capsule())

切割、描边、阴影

Image("xixi")// 边缘形状.clipShape(Circle())// 描边.overlay(Circle().stroke(Color.yellow,lineWidth:2))// 阴影.shadow(color:.blue,radius:20)

image.png

SwiftUI为什么使用结构体而不是用类?

首先,有一个性能因素:结构体比类更简单,更快。我之所以说性能因素,是因为很多人认为这是SwiftUI使用结构体的主要原因,而实际上这只是更大范围的一部分。

在UIKit中,每个视图都来自一个名为UIView的类,该类具有许多属性和方法:背景色,确定其放置方式的约束,用于将其内容呈现到其中的图层等等。其中有很多,每个UIView和UIView子类都必须具有它们,因为继承是这样工作的。

视图作为结构体还是有很多更重要的事情:它迫使我们考虑以一种干净的方式隔离状态。您会发现,类能够自由更改其值,这可能导致代码混乱。

通过生成不会随时间变化的视图,SwiftUI鼓励我们转向更具功能性的设计方法:在将数据转换为UI时,我们的视图变成简单的,惰性的东西,而不是会失去控制的智能化的东西。

当您查看可以作为视图的事物时,可以看到这一点。我们已经使用了Color.red和LinearGradient作为视图——包含很少数据的简单类型。实际上,您不能找到比使用Color.red作为视图的更好的主意:除了“用红色填充我的空间”之外,它不包含任何信息。

相比之下,Apple的UIView文档列出了UIView拥有的约200种属性和方法,无论是否需要它们,所有这些属性和方法都将传递给其子类。

提示:如果您在视图中使用类,则可能会发现代码无法编译或在运行时崩溃。

SwiftUI应用点语法修改视图,返回的也是视图类型。每次我们修改视图时,SwiftUI都会使用以下泛型来应用该修饰符:ModifiedContent<OurThing, OurModifier>。

为什么SwiftUI使用some View作为视图类型?

返回some View与仅返回View相比有两个重要区别:

1、我们必须始终返回相同类型的视图。

2、即使我们不知道返回的视图类型,编译器也同样不知道。

这种代码是不允许的:

varbody:some View{ifself.useRedText{returnText("Hello World")}else{returnText("Hello World").background(Color.red)}}

some View意味着“将返回一种特定类型的视图,但我们不想说它是什么。”由于SwiftUI使用通用的ModifiedContent包装器创建新视图的方式,Text(…)和Text(…).background(Color.red)是不同的底层类型,这与some View不兼容。

SwiftUI使用ModifiedContent构建数据的方式。

SwiftUI是如何处理VStack这样的东西的——它符合View协议,如果您创建了一个包含两个文本视图的VStack,那么SwiftUI会无声地创建一个TupleView来包含这两个视图。TupleView的一个版本可以跟踪十种不同的内容:

TupleView<(C0,C1,C2,C3,C4,C5,C6,C7,C8,C9)>

这就是为什么SwiftUI在一个父级中不允许超过10个视图的原因:他们编写了TupleView的版本,可以处理2到10个视图,但不能超过10个。

自定义修饰符,使用ViewModifier

importSwiftUIstructMyViewModifier:View{varbody:someView{VStack{Text("Hello, World!")// 使用修饰符.modifyTitle()Text("Hello, World!")// 使用修饰符.modifySubTitle(text:"前缀")}}}structMyViewModifier_Previews:PreviewProvider{staticvarpreviews:someView{MyViewModifier()}}// 自定义修饰符structTitle:ViewModifier{funcbody(content:Content)->someView{content.font(.title).foregroundColor(.white).padding().background(Color.red).clipShape(RoundedRectangle(cornerRadius:5.0))}}// 自定义修饰符,并重新构建视图structSubTitle:ViewModifier{vartext:Stringfuncbody(content:Content)->someView{VStack{content.font(.subheadline).foregroundColor(.gray).padding().background(Color.green).clipShape(RoundedRectangle(cornerRadius:5.0))Text(text).font(.subheadline).foregroundColor(.blue)}}}// 扩展修饰符extensionView{funcmodifyTitle()->someView{self.modifier(Title())}funcmodifySubTitle(text:String)->someView{self.modifier(SubTitle(text:text))}}

LazyVGrid和LazyHGrid使用(iOS14新增)

lettext=(1...10).map{"Hello\($0)"}// 以最小宽度160斤可能在一行放入gridletcolumns=[GridItem(.adaptive(minimum:80))]// 每行三个grids,大小灵活分配letcolumnsFixed=[GridItem(.flexible()),GridItem(.flexible()),GridItem(.flexible()),]// 第一个100固定,第二个尽量填满letcolumnsFixed100=[GridItem(.fixed(100)),GridItem(.flexible()),]varrows:[GridItem]=Array(repeating:.init(.fixed(20)),count:2)varbody:someView{ScrollView{Section(header:Text("最小160")){LazyVGrid(columns:columns,spacing:20){ForEach(text,id:\.self){iteminText(item)}}}Section(header:Text("每行三个Grid")){LazyVGrid(columns:columnsFixed,spacing:20){ForEach(text,id:\.self){iteminText(item)}}}Section(header:Text("第一个固定100")){LazyVGrid(columns:columnsFixed100,spacing:20){ForEach(text,id:\.self){iteminButton(item){print("itme pressed")}}}}ScrollView(.horizontal){LazyHGrid(rows:rows,alignment:.top){ForEach(0...79,id:\.self){letcodepoint=$0+0x1F600letcodepointString=String(format:"%02X",codepoint)Text("\(codepointString)").font(.footnote)letemoji=String(Character(UnicodeScalar(codepoint)!))Text("\(emoji)").font(.largeTitle)}}}}}

image.png

ForEach使用区别:

letagents=["Cyril","Lana","Pam","Sterling"]VStack{ForEach(0..<agents.count){Text(self.agents[$0])}}

我们回到Swift如何识别数组中的值。当我们使用0..<5或0..<agents.count这样的范围时,Swift确信每个项目都是唯一的,因为它将使用范围中的数字——每个数字在循环中只使用一次,所以它肯定是唯一的。

但是当使用字符串时,不会标识为唯一,导致body被调用时会被重建。因此可以使用id来标识,如下:

VStack{ForEach(agents,id:\.self){Text($0)}}

另外,为了标识视图的唯一,可以用Identifiable协议来实现:

定义:

@available(macOS10.15,iOS13,tvOS13,watchOS6,*)publicprotocolIdentifiable{/// A type representing the stable identity of the entity associated with/// an instance.associatedtypeID:Hashable/// The stable identity of the entity associated with this instance.varid:Self.ID{get}}

使用:

structModelData:Identifiable{varid:Int}

使用定制绑定Binding

简单使用:

structContentView:View{@Stateprivatevarselection=0varbody:someView{letbinding=Binding(get:{self.selection},set:{self.selection=$0})returnVStack{Picker("Select",selection:binding){ForEach(0..<3){Text("Item\($0)")}}.pickerStyle(SegmentedPickerStyle())Text("\(selection)")}}}

因此,该绑定实际上只是作为一个传递——它本身不存储或计算任何数据,而是作为UI和被操纵的底层状态值之间的一个填充。

但是,请注意,选择器现在是使用selection:binding进行的,不需要美元符号。我们不需要在这里明确要求双向绑定,因为它已经是了。

高级使用:

structContentView:View{@StateprivatevaragreedToTerms=false@StateprivatevaragreedToPrivacyPolicy=false@StateprivatevaragreedToEmails=falsevarbody:someView{letagreeToAll=Binding<Bool>(get:{self.agreedToTerms&&self.agreedToPrivacyPolicy&&self.agreedToEmails},set:{self.agreedToTerms=$0self.agreedToPrivacyPolicy=$0self.agreedToEmails=$0})returnVStack{Toggle(isOn:$agreedToTerms){Text("agreedToTerms")}Toggle(isOn:$agreedToPrivacyPolicy){Text("agreedToPrivacyPolicy")}Toggle(isOn:$agreedToEmails){Text("agreedToEmails")}Toggle(isOn:agreeToAll){Text("agreeToAll")}}.padding()}}

image.png

double转String保留几位小数

// 保留2位小数Text("\(sleepAmount, specifier: "%.2f") 小时")// 保留2位小数,并自动删除末尾不需要的0Text("\(sleepAmount, specifier: "%.2g") 小时")

image.png

DatePicker使用

struct DatePickerView:View{@StateprivatevarwakeUp=Date()varbody:some View{VStack{// 有标题DatePicker("Please enter a date",selection:$wakeUp)// 无标题DatePicker("Please enter a date",selection:$wakeUp).labelsHidden()// 无标题,有时间范围DatePicker("Please",selection:$wakeUp,in:Date()...Date().addingTimeInterval(86400)).labelsHidden()DatePicker("Please",selection:$wakeUp,in:Date()...).labelsHidden()}}}

image.png

DateComponents和DateFormatter使用

// hour、minute通过DateComponents生成DatevardateComponents=DateComponents()dateComponents.hour=8letdate=Calendar.current.date(from:dateComponents)// Date通过DateComonents获取hour、minuteletsomeDate=Date()letcomponents=Calendar.current.dateComponents([.hour,.minute],from:someDate)lethour=components.hour??0letminute=components.minute??0// Date转StringletdateFormatter=DateFormatter()dateFormatter.dateStyle=.shortletdateString=dateFormatter.string(from:Date())

SwiftUI读取本地项目文件

ifletstartWordsUrl=Bundle.main.url(forResource:"start",withExtension:"txt"){ifletstartWords=try?String(contentsOf:startWordsUrl){letallWords=startWords.components(separatedBy:"\n")rootWord=allWords.randomElement()??"silkworm"return}}

显示视图时调用的闭包onAppear

NavigationView {            VStack {                TextField("输入单词", text: $newWord, onCommit: addNewWord)                .textFieldStyle(RoundedBorderTextFieldStyle())                .padding()                    .autocapitalization(.none)                List(usedWords, id: \.self) {                    Image(systemName: "\($0.count).circle")                    Text($0)                }            }            .navigationTitle(rootWord)            .onAppear(perform: startGame)        }

使用多个.animation对不同的动画,进行分别处理

structContentView:View{@Stateprivatevarenabled=falsevarbody:someView{VStack{Button("Tap Me"){self.enabled.toggle()}.frame(width:200,height:200).background(enabled?Color.blue:Color.red).animation(.default)// 针对颜色的动画.foregroundColor(.white).clipShape(RoundedRectangle(cornerRadius:enabled?60:0)).animation(.interpolatingSpring(stiffness:10,damping:1))// 针对形状的动画}}}

禁用动画:.animation(nil)

手势动画

structContentView:View{@StateprivatevardragAmount=CGSize.zerovarbody:someView{LinearGradient(gradient:Gradient(colors:[.yellow,.red]),startPoint:.topLeading,endPoint:.bottomTrailing).frame(width:300,height:200).clipShape(RoundedRectangle(cornerRadius:10))// 视图位置改变.offset(dragAmount)// 添加拖动手势.gesture(DragGesture().onChanged{// 实时根据手势位置改变视图的位置self.dragAmount=$0.translation}.onEnded{_in// 弹性动画归0withAnimation(.spring()){self.dragAmount=.zero}})}}

结构体和类的使用区别

结构体一直拥有唯一的所有者,而对于类,多个对象可以指向同一个值。

类不需要在更改其属性的方法之前使用mutating关键字,因为您可以更改常量类的属性。

实际上,这意味着,如果我们有两个SwiftUI视图,并且我们将同一个结构体发送给它们,那么它们实际上都有该结构体的唯一副本;如果其中一个更改了它,那么另一个将看不到该更改。另一方面,如果我们创建一个类的实例并将其发送到两个视图,它们将共享更改。

对于SwiftUI开发人员来说,这意味着如果我们想要在多个视图之间共享数据——如果我们想要两个或多个视图指向同一个数据,以便在一个更改时它们都得到这些更改——我们需要使用类而不是结构体。

为什么使用@ObservedObject

如果使用类的同时,使用@State来让SwiftUI监听值的改变,虽然值改变了,但是SwiftUI监听不到类中值的改变,不会对body进行销毁和重建,所以需要使用@OvervedObject来处理该问题。

当我们使用@State时,我们要求SwiftUI监视属性的更改。因此,如果我们更改字符串、翻转布尔值、添加到数组等,则属性已更改,SwiftUI将重新调用视图的body属性。

当User是一个结构体时,每次我们修改该结构体的属性时,Swift实际上是在创建该结构的新实例。@State能够发现这个变化,并自动重新加载我们的视图。现在我们有了一个类,这种行为不再发生:Swift可以直接修改值。

还记得我们如何为修改属性的结构体方法使用mutating关键字吗?这是因为,如果我们将结构体的属性创建为变量,但结构体本身是常量,则无法更改属性——Swift需要能够在属性更改时销毁和重新创建整个结构体,而常量结构则不可能这样做。类不需要mutating关键字,因为即使类实例标记为常量Swift,仍然可以修改变量属性。

我知道这听起来非常理论化,但这里有个问题:既然User是一个类,那么属性本身不会改变,所以@State不会注意到任何事情,也无法重新加载视图。是的,类中的值正在更改,但是@State不监视这些值,所以实际上发生的情况是,类中的值正在更改,但视图不会重新加载以反映该更改。

为了解决这个问题,是时候抛弃@State了。相反,我们需要一个更强大的属性包装器,名为@ObservedObject。

@ObservedObject、@Published的使用

如果需要在多个视图之间共享数据的话,可使用@ObservedObject和@EnvironmentObject。

@Published用于类中,通知关注类的所有视图在类发生改变时,去重新加载。

classUser{// @Published通知关注类的所有视图在类发生改变时,去重新加载@PublishedvarfirstName="zhiying"@PublishedvarlastName="yuan"}

@ObservedObject用于获知知道哪些类在改变时能通知视图,它告诉SwiftUI监视类中的任何的更改公告。

@ObservedObject属性包装器只能用于符合ObservableObject协议的类型。该协议没有任何要求,实际上意味着“我们希望其他事物能够监视此更改”。

// 类遵守ObservableObject协议classUser:ObservableObject{// @Published 通知关注类的所有视图在类发生改变时,去重新加载@PublishedvarfirstName="zhiying"@PublishedvarlastName="yuan"}struct ContentView:View{// @ObservedObject 用于标记哪些类在改变时通知视图加载视图@ObservedObjectvaruser=User()varbody:some View{VStack{Text("名字是\(user.firstName)\(user.lastName)")TextField("firstName",text:$user.firstName)TextField("lastName",text:$user.lastName)}}}

三个步骤:

创建一个符合ObservableObject协议的类。

用@Published标记一些属性,以便使用该类的所有视图在更改时都得到更新。

使用@ObservedObject属性包装器创建我们的类的实例。

弹出模态视图,并通过获取全局变量来关闭模态视图

弹出

structContentView:View{@StateprivatevarshowSheet=falsevarbody:someView{VStack{Button("show sheet"){self.showSheet.toggle()}.sheet(isPresented:$showSheet,content:{SecondView()})}}}

关闭

structSecondView:View{// 获取全局环境变量 presentationMode@Environment(\.presentationMode)varsecondPresentationModevarbody:someView{Button("关闭"){// 通过获取到的全局环境变量,来关闭模态视图self.secondPresentationMode.wrappedValue.dismiss()}}}

SwiftUI中\.self是什么?

[SwiftUI 100天] Core Data ForEach .self 的工作机制

struct ContentView:View{@Stateprivatevarnumbers=[Int]()varbody:some View{VStack{List{ForEach(numbers,id:\.self){Text("\($0)")}}}}}

之前我们了解了使用ForEach来创建动态视图的不同方式,它们都有一个共同点:SwiftUI 需要知道如何唯一识别动态视图的每一项,以便正确地动画化改变。

如果一个对象遵循Identifiable协议,那么 SwiftUI 会自动使用它的id属性作为唯一标识。如果我们不使用Identifiable,那就需要指定一个我们知道是唯一的属性的 key path,比如图书的 ISBN 号。但假如我们不遵循Identifiable也没有唯一的 key path,我们通常会使用.self。

我们对原始数据类型,例如Int和String使用过.self,就像下面这样:

List {

ForEach([2, 4, 6, 8, 10], id: .self) {

Text("($0) is even")

}

}

对于 Core Data 为我们生成托管类,我们也使用.self,当时我没有解释这是如何工作的,或者说它究竟是如何与我们的ForEach关联的。不过今天我要再来讨论这个问题,因为我觉得这会给你提供一些有助益的洞察。

当我们使用.self作为标识符时,我们指的是“整个对象”,但实践上这个指代并没有包含太多的含义 —— 一个结构体就是结构体,除了内容之外,它并没有包含任何特定的标识信息。因此,实际发生的事情是,Swift 计算了结构体的哈希值 —— 一种以固定长度的值表示复杂数据的方法 —— 然后以哈希值作为标识符。

哈希值可以以很多种方法生成,但所有方法的背后的概念是一致的:

无论输入的数据多大,输出总是固定大小。

对同一个对象计算两次哈希值,应该返回相同的值。

这两点听起来很简单,但你思考一下:假设我们获取 “Hello World” 的哈希值和莎士比亚全集的哈希值,两者都将是相同的大小。这意味着我们是无法从哈希值还原出原始数据的 —— 我们无法从 40 个看起来完全随机的十六进制数字转换出莎士比亚全集。

哈希常见于数据校验。举个例子,假如你下载了一个 8GB 的 zip 文件,你可以通过对比你本地的哈希值和服务器上的哈希值来确认文件是正确的 —— 如果两者匹配,说明 zip 文件是一致的。哈希还被用于字典的键和值,这是为什么字典查询速度很快的原因。

上面说的这些很重要,因为 Xcode 为我们生成托管对象的类时,它会让这些类遵循Hashable,这是一个表示 Swift 可以从中生成哈希值的协议,也就是说,我们可以用它的.self作为标识符。这也是为什么String和Int可以用.self的原因:它们也遵循Hashable。

Hashable有点类似Codable:如果我们想让一个自定义类型遵循Hashable,那么只要它包含的所有东西也遵循Hashable,那我们就不必做额外的工作。为了说明这一点,我们可以创建一个自定义结构体,让它遵循Hashable而不是Identifiable,然后使用.self来标识它:

struct Student: Hashable {

let name: String

}

struct ContentView: View {

let students = [Student(name: "Harry Potter"), Student(name: "Hermione Granger")]

varbody:some View{List(students,id:\.self){student inText(student.name)}}

}

我们可以让Student遵循Hashable,因为它所有的属性都已经遵循Hashable,因此 Swift 会计算每个属性的哈希值,然后结合这些值产生一个代表整个对象的哈希值。当然,如果我们遇到两个同名的学生,那我们可能会遇到问题,这就像我们拥有一个包含两个相同字符串的字符串数组一样。

现在,你可能想,这样会导致问题吧:如果我们用相同的值创建了两个 Core Data 对象,它们会生成相同的哈希值,这样我们就遇到问题了。不过,其实 Core Data 是一种很聪明的方式来工作:它为我们创建的对象实际上有一组我们定义数据模型时定义的属性之外的其他属性,包括一个叫 ID 的对象 —— 这是一个可以唯一标识对象的标识符,不管对象包含的属性是什么。这些 ID 类似于 UUID,在我们创建对象时,Core Data 顺序产生它们。

因此,.self适用于所有遵循Hashable的类,因为 Swift 会为其生成哈希值并用该值作为对象的唯一标识。这对于 Core Data 的对象同样适用,因为它们已经遵循了Hashable。

警告: 虽然给一个对象计算两次哈希值应该返回相同的值,但在两次应用运行期间计算它们 —— 比如说,计算哈希值,退出应用,重启,然后再计算哈希值 —— 是有可能返回不同的值的。

onDelete()的使用

单个左滑删除

struct ContentView:View{@Stateprivatevarnumbers=[Int]()@StateprivatevarcurrentNumber=1varbody:some View{VStack{List{ForEach(numbers,id:\.self){Text("\($0)")}// onDelete只能添加在ForEach上.onDelete(perform:{indexSetin// ForEach是由numbers数组创建的,可以直接将索引集直接传给numbers数组numbers.remove(atOffsets:indexSet)})}Button("添加"){numbers.append(currentNumber)currentNumber+=1}}}}

image.png

多个点击删除

struct ContentView:View{@Stateprivatevarnumbers=[Int]()@StateprivatevarcurrentNumber=1varbody:some View{NavigationView{VStack{List{ForEach(numbers,id:\.self){Text("\($0)")}.onDelete(perform:{indexSetinnumbers.remove(atOffsets:indexSet)})}Button("添加"){numbers.append(currentNumber)currentNumber+=1}}.navigationBarItems(leading:EditButton())}}}

image.png

本地数据存储UserDefaults

存UserDefaults.standard.setValue(self.tapCount,forKey:"tapCount")取(未设置有默认值)UserDefaults.standard.integer(forKey:"tapCount")

调整图片大小,以适应屏幕

structContentView:View{varbody:some View{NavigationView{VStack{// GeometryReader 确保图像填充其容器视图的整个宽度GeometryReader(content:{geometryinImage("WX20210226-120815").resizable().aspectRatio(contentMode:.fit).frame(width:geometry.size.width)})VStack{Text("哈哈哈哈哈")Text("哈哈哈哈哈")Text("哈哈哈哈哈")Text("哈哈哈哈哈")Text("哈哈哈哈哈")}}.navigationTitle(Text("我是标题"))}}}

image.png

structContentView:View{varbody:some View{NavigationView{VStack{// GeometryReader 确保图像填充其容器视图的整个宽度GeometryReader(content:{geometryinImage("WX20210226-120815").resizable().aspectRatio(contentMode:.fit).frame(width:geometry.size.width)})VStack{Text("哈哈哈哈哈")Text("哈哈哈哈哈")Text("哈哈哈哈哈")Text("哈哈哈哈哈")Text("哈哈哈哈哈")}VStack{Text("哈哈哈哈哈")Text("哈哈哈哈哈")Text("哈哈哈哈哈")Text("哈哈哈哈哈")Text("哈哈哈哈哈")}VStack{Text("哈哈哈哈哈")Text("哈哈哈哈哈")Text("哈哈哈哈哈")Text("哈哈哈哈哈")Text("哈哈哈哈哈")}VStack{Text("哈哈哈哈哈")Text("哈哈哈哈哈")Text("哈哈哈哈哈")Text("哈哈哈哈哈")Text("哈哈哈哈哈")}VStack{Text("哈哈哈哈哈")Text("哈哈哈哈哈")Text("哈哈哈哈哈")Text("哈哈哈哈哈")Text("哈哈哈哈哈")}}.navigationTitle(Text("我是标题"))}}}

image.png

List + ForEach与ScrollView + ForEach区别

List + ForEach会在可见的时候才创建

ScrollView + ForEach会一次性创建所有视图

List + ForEach

struct ContentView:View{varbody:some View{NavigationView{// List + ForEach 会在可见的时候才创建List{ForEach(0..<100){CustomText("\($0) _")}}.navigationTitle(Text("标题"))}}}

ScrollView + ForEach

structContentView:View{varbody:someView{NavigationView{ScrollView{ForEach(0..<100){CustomText("\($0)_")}.frame(maxWidth:.infinity)}.navigationTitle(Text("标题"))}}}

布局优先级layoutPriority

所有视图的默认优先级均为0

structContentView:View{varbody:someView{HStack(spacing:16){Text("Hello")Text("World")// 布局优先级layoutPriority,所有视图的默认优先级均为0Text("哈哈哈哈哈哈哈").layoutPriority(1)}.font(.largeTitle).lineLimit(1)}}

image.png

Path绘制线

structContentView:View{var body:some View{Path({pathinpath.move(to:CGPoint(x:200,y:100))path.addLine(to:CGPoint(x:100,y:300))path.addLine(to:CGPoint(x:300,y:300))path.addLine(to:CGPoint(x:200,y:100))})// style - StrokeStyle用来控制每条线的连接方式.stroke(Color.blue.opacity(0.5),style:StrokeStyle(lineWidth:5,lineCap:.round,lineJoin:.round,miterLimit:20,dash:[15],dashPhase:55))}}

image.png

stride使用

从起始值以指定值步幅到结束值的序列

从0度到360度,每22.5度一步生成一个序列stride(from:0,to:CGFloat.pi*2,by:CGFloat.pi/8)

stroke绘制边框

Circle().stroke(Color.blue,lineWidth:4).padding(100)

image.png

循环绘制形状

structContentView:View{@StateprivatevarpetalOffset=-20.0@StateprivatevarpetalWidth=100.0varbody:someView{VStack{Flower(petalOffset:petalOffset,petalWidth:petalWidth).fill(// 填充渐变色AngularGradient(gradient:Gradient(colors:[.red,.yellow,.green,.blue,.purple,.red]),center:.center),// eoFill: 奇偶填充style:FillStyle(eoFill:true))Text("Offset")Slider(value:$petalOffset,in:-40...40).padding([.horizontal,.bottom])Text("Width")Slider(value:$petalWidth,in:0...100).padding(.horizontal)}.padding(20)}}structFlower:Shape{// 花瓣移离中心多少距离varpetalOffset:Double=-20// 每片花瓣的宽度varpetalWidth:Double=100funcpath(inrect:CGRect)->Path{// 容纳所有花瓣的路径varpath=Path()// 从0向上计数到 pi * 2,每次移动 pi / 8fornumberinstride(from:0,to:CGFloat.pi*2,by:CGFloat.pi/8){// 根据循环旋转当前的花瓣letrotation=CGAffineTransform(rotationAngle:number)// 将花瓣移到我们视野的中心letposition=rotation.concatenating(CGAffineTransform(translationX:rect.width/2,y:rect.height/2))// 使用我们的属性以及固定的Y和高度为该花瓣创建路径letoriginalPetal=Path(ellipseIn:CGRect(x:CGFloat(petalOffset),y:0,width:CGFloat(petalWidth),height:rect.width/2))// 将我们的旋转/位置变换应用于花瓣letrotatedPetal=originalPetal.applying(position)// 将其添加到我们的主路径path.addPath(rotatedPetal)}// 现在将主径 returnreturnpath}}

image.png

ImagePaint 制作边框和填充

structContentView:View{varbody:someView{VStack{// sourceRect 相对大小和位置的CGRect 0表示“开始”,1表示“结束”// scale 使用比例尺绘制示例图像,0.2表示该图像的显示尺寸为正常尺寸的1/5Text("1111").frame(width:180,height:180,alignment:.center).border(ImagePaint(image:Image("WX20210310-163132"),sourceRect:CGRect(x:0,y:0.25,width:1,height:0.5),scale:0.1),width:20)}}}

image.png

启用Metal高性能渲染

SwiftUI默认使用CoreAnimation来进行渲染,但是遇到复杂的渲染,可以启用高性能渲染Metal。

structContentView:View{@StateprivatevarcolorCycle=0.0varbody:someView{VStack{ColorCyclingCircle(amount:self.colorCycle).frame(width:300,height:300)Slider(value:$colorCycle)}}}structColorCyclingCircle:View{varamount=0.0varsteps=100varbody:someView{ZStack{ForEach(0..<steps){valueinCircle().inset(by:CGFloat(value)).strokeBorder(LinearGradient(gradient:Gradient(colors:[self.color(for:value,brightness:1),self.color(for:value,brightness:0.5)]),startPoint:.top,endPoint:.bottom),lineWidth:2)}}.drawingGroup()}funccolor(forvalue:Int,brightness:Double)->Color{vartargetHue=Double(value)/Double(self.steps)+self.amountiftargetHue>1{targetHue-=1}returnColor(hue:targetHue,saturation:1,brightness:brightness)}}

image.png

通过应用一个称为drawingGroup()的新修改器来解决此问题。这告诉SwiftUI,在将视图内容作为单个呈现的输出放回到屏幕上之前,应将视图的内容呈现到屏幕外的图像中(离屏渲染),这要快得多。在幕后,该功能由Metal提供支持,Metal是Apple的框架,可直接与GPU协同工作以实现极快的图形。

重要提示:drawingGroup()修饰符有助于了解和保留您的武器库,这是解决性能问题的一种方法,但是您不应该经常使用它。添加屏幕外渲染过程可能会降低SwiftUI进行简单绘图的速度,因此,在尝试引入drawingGroup()之前,应等到遇到实际性能问题后再进行操作。

74、实现实施模糊、混合模式、饱和度调整等效果

SwiftUI使我们能够出色地控制视图的呈现方式,包括应用实时模糊,混合模式,饱和度调整等功能。

混合模式使我们可以控制一个视图在另一个视图上的渲染方式。默认模式是.normal,它只是将新视图中的像素绘制到后面的任何东西上,但是有很多选项可以控制颜色和不透明度。

structContentView:View{@StateprivatevarcolorCycle=0.0varbody:someView{VStack{ZStack{Image("demo1")Rectangle().fill(Color.blue)// blendMode图像混合模式 默认normal.blendMode(.softLight).frame(width:500,height:500,alignment:.center)}Image("demo1").colorMultiply(.yellow)}}}

image.png

之所以命名为“Multiply”,是因为它将每个源像素颜色与目标像素颜色相乘——在我们的示例中,是图像的每个像素和顶部的矩形的每个像素。每个像素具有RGBA的颜色值,范围从0(没有该颜色)到1(所有颜色),因此所得的最高颜色为1x1,最低的颜色为0x0。

对纯色使用乘法会产生一种非常常见的色调效果:黑色保持黑色(因为它们的颜色值为0,所以无论您将顶部乘以0都将产生0),而较浅的颜色会变成各种阴影着色。

混合模式screen,它的作用与乘法相反:将颜色反转,执行乘法,然后再次反转颜色,从而产生较亮的图像而不是较暗的图像。

常用用法:.colorMultiply(Color.red)

structContentView:View{@Stateprivatevaramount:CGFloat=0.0varbody:some View{VStack{ZStack{Circle()//                    .fill(Color.red).fill(Color(red:1,green:0,blue:0)).frame(width:200*amount).offset(x:-50,y:-80).blendMode(.screen)Circle()//                    .fill(Color.green).fill(Color(red:0,green:1,blue:0)).frame(width:200*amount).offset(x:50,y:-80).blendMode(.screen)Circle()//                    .fill(Color.blue).fill(Color(red:0,green:0,blue:1)).frame(width:200*amount).blendMode(.screen)}.frame(width:300,height:300)Slider(value:$amount).padding()}.frame(maxWidth:.infinity,maxHeight:.infinity).background(Color.black).edgesIgnoringSafeArea(.all)}}

image.png

模糊效果

structContentView:View{@Stateprivatevaramount:CGFloat=0.0varbody:some View{VStack{// 模糊效果Image("demo1").resizable().scaledToFit().frame(width:200,height:200).saturation(Double(amount))// 饱和度,用于调整颜色的数量.blur(radius:(1-amount)*20)Slider(value:$amount).padding()}}}

image.png

75、edgesIgnoringSafeArea边界忽略safeArea安全区域

76、Shape形状设置动画(单个动画变量)

structContentView:View{@StateprivatevarinsetAmount:CGFloat=50@Stateprivatevarrows=4@Stateprivatevarcolumns=4varbody:someView{Trapezoid(insetAmount:insetAmount).frame(width:200,height:100).onTapGesture{// 添加动画withAnimation(.linear(duration:1)){self.insetAmount=CGFloat.random(in:10...90)}}}}structTrapezoid:Shape{varinsetAmount:CGFloat// 使用 animatableData 给形状设置动画varanimatableData:CGFloat{get{insetAmount}set{self.insetAmount=newValue}}funcpath(inrect:CGRect)->Path{varpath=Path()path.move(to:CGPoint(x:0,y:rect.maxY))path.addLine(to:CGPoint(x:insetAmount,y:rect.minY))path.addLine(to:CGPoint(x:rect.maxX-insetAmount,y:rect.minY))path.addLine(to:CGPoint(x:rect.maxX,y:rect.maxY))path.addLine(to:CGPoint(x:0,y:rect.maxY))returnpath}}

这里发生的事情非常复杂:当我们使用withAnimation()时,SwiftUI会立即将状态属性更改为其新值,但在幕后,随着动画的进行,它还在跟踪随时间的值变化。随着动画的进行,SwiftUI会将 Shape 的animatableData属性设置为最新值,这取决于我们来决定这意味着什么——在本例中,我们将其直接分配给insetAmount,因为这就是我们要进行动画处理的东西。

记住,SwiftUI在应用动画之前先评估视图状态,然后再应用动画。可以看到我们最初有评估为Trapezoid(insetAmount:50)的代码,但是在选择了一个随机数之后,我们最终得到了(例如)Trapezoid(insetAmount:62)。因此,它将在动画的整个长度内插值50到62,每次将形状的animatableData属性设置为最新的插值:51、52、53,依此类推,直到达到62。

77、Shape形状设置动画(多个动画变量)

structContentView:View{@Stateprivatevarrows=4@Stateprivatevarcolumns=4varbody:someView{Checkerboard(rows:rows,columns:columns).onTapGesture{// 添加动画withAnimation(.linear(duration:1)){self.rows=8self.columns=16}}}}structCheckerboard:Shape{varrows:Intvarcolumns:IntpublicvaranimatableData:AnimatablePair<Double,Double>{get{AnimatablePair(Double(rows),Double(columns))}set{self.rows=Int(newValue.first)self.columns=Int(newValue.second)}}funcpath(inrect:CGRect)->Path{varpath=Path()// 计算每行/列需要多大letrowSize=rect.height/CGFloat(rows)letcolumnSize=rect.width/CGFloat(columns)// 循环遍历所有行和列,从而使交替的正方形变为彩色forrowin0..<rows{forcolumnin0..<columns{if(row+column).isMultiple(of:2){// 这个正方形应该是彩色的;在此处添加一个矩形letstartX=columnSize*CGFloat(column)letstartY=rowSize*CGFloat(row)letrect=CGRect(x:startX,y:startY,width:columnSize,height:rowSize)path.addRect(rect)}}}returnpath}}

78、为@Published包装器添加Codable支持

使用:

importFoundationclassUser:ObservableObject,Codable{@Publishedvarname="xixi"enumCodingKeys:CodingKey{casename}requiredinit(from decoder:Decoder)throws{// decoder包含了所有的数据letcontainer=trydecoder.container(keyedBy:CodingKeys.self)name=trycontainer.decode(String.self,forKey:.name)}funcencode(to encoder:Encoder)throws{varcontainer=encoder.container(keyedBy:CodingKeys.self)trycontainer.encode(name,forKey:.name)}}

原因:使用@Published包装后的属性,被包装在了另外一个类型中,这个类型包含一些其他的功能。比如Published<String>,是一个包含字符串的可发布的对象。

79、使用URLSession和URLRequest请求数据

structContentView:View{@Stateprivatevarresults=[Result]()varbody:someView{List(results,id:\.trackId){iteminVStack(alignment:.leading){Text(item.trackName).font(.headline)Text(item.collectionName)}}.onAppear(perform:loadData)}funcloadData(){guardleturl=URL(string:"https://itunes.apple.com/search?term=taylor+swift&entity=song")else{print("Invalid URL")return}letrequest=URLRequest(url:url)URLSession.shared.dataTask(with:request){(data,response,error)inifletdata=data{ifletresponseData=try?JSONDecoder().decode(Response.self,from:data){DispatchQueue.main.async{self.results=responseData.results}}}}.resume()}}structContentView_Previews:PreviewProvider{staticvarpreviews:someView{ContentView()}}structResponse:Codable{varresults:[Result]}structResult:Codable{vartrackId:IntvartrackName:StringvarcollectionName:String}

80、disabled来控制控件是否可用

struct ContentView:View{@Stateprivatevarusername=""@Stateprivatevaremail=""varbody:some View{Form{Section{TextField("Username",text:$username)TextField("Email",text:$email)}Section{Button("Create account"){print("Creating account…")}.disabled(username.isEmpty||email.isEmpty)}}}}

image.png

81、使用@Binding创建自定义视图,实现双向绑定

structPushButton:View{lettitle:String@BindingvarisOn:BoolvaronColors=[Color.red,Color.yellow]varoffColors=[Color(white:0.6),Color(white:0.4)]varbody:someView{Button(title){self.isOn.toggle()}.padding().background(LinearGradient(gradient:Gradient(colors:isOn?onColors:offColors),startPoint:.leading,endPoint:.trailing)).foregroundColor(.white).clipShape(Capsule()).shadow(radius:10)}}

struct ContentView:View{@StateprivatevarrememberMe=falsevarbody:some View{NavigationView{List{PushButton(title:"Remember Me",isOn:$rememberMe)Text(rememberMe?"开启":"关闭")}.navigationBarTitle("Cupcake Corner")}}}

如果不使用@Binding,外部页面中,使用外部页面的属性创建自定义页面,只是传入自定义页面参数,传入后,里面值的改变并不会传递到外面。

自定义页面中,将需要绑定的属性使用@Binding修饰符,绑定外部页面的属性,将自定义页面中的值的改变传递到外部页面中,同步改变。

82、使用CoreData来增删改查数据

相关文章:SwiftUI CoreData入门概念和基础大全

创建持久化控制器单例PersistenceController,并创建初始化持久化容器NSPersistentContainer

importCoreDatastructPersistenceController{staticletshared=PersistenceController()staticvarpreview:PersistenceController={letresult=PersistenceController(inMemory:true)letviewContext=result.container.viewContextfor_in0..<10{letnewItem=Item(context:viewContext)newItem.timestamp=Date()}do{tryviewContext.save()}catch{// Replace this implementation with code to handle the error appropriately.// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.letnsError=errorasNSErrorfatalError("Unresolved error\(nsError),\(nsError.userInfo)")}returnresult}()letcontainer:NSPersistentContainerinit(inMemory:Bool=false){container=NSPersistentContainer(name:"CoreDataSwiftUIDemo")ifinMemory{container.persistentStoreDescriptions.first!.url=URL(fileURLWithPath:"/dev/null")}container.loadPersistentStores(completionHandler:{(storeDescription,error)inifleterror=errorasNSError?{// Replace this implementation with code to handle the error appropriately.// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development./*

                Typical reasons for an error here include:

                * The parent directory does not exist, cannot be created, or disallows writing.

                * The persistent store is not accessible, due to permissions or data protection when the device is locked.

                * The device is out of space.

                * The store could not be migrated to the current model version.

                Check the error message to determine what the actual problem was.

                */fatalError("Unresolved error\(error),\(error.userInfo)")}})}}

在app中创建全局持久化控制器,并将持久化控制器的持久化容器的上下文注入全局环境变量中

importSwiftUI@mainstructCoreDataSwiftUIDemoApp:App{letpersistenceController=PersistenceController.sharedvarbody:someScene{WindowGroup{ContentView().environment(\.managedObjectContext,persistenceController.container.viewContext)}}}

创建项目同名的DataModel文件,后缀为.xcdatamodelId,创建entity

image.png

从全局环境变量中取出上下文

@Environment(\.managedObjectContext) private var viewContext

使用@FetchRequest装饰器,从数据库中读取指定entity的数据列表

@FetchRequest(sortDescriptors:[NSSortDescriptor(keyPath:\Item.timestamp,ascending:true)],animation:.default)privatevaritems:FetchedResults<Item>

往coredata添加数据,通过从全局环境变量中获取到的上下文,创建对象

privatefuncaddItem(){withAnimation{letnewItem=Item(context:viewContext)newItem.timestamp=Date()do{tryviewContext.save()}catch{// Replace this implementation with code to handle the error appropriately.// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.letnsError=errorasNSErrorfatalError("Unresolved error\(nsError),\(nsError.userInfo)")}}}

删除coredata数据

privatefuncdeleteItems(offsets:IndexSet){withAnimation{offsets.map{items[$0]}.forEach(viewContext.delete)do{tryviewContext.save()}catch{// Replace this implementation with code to handle the error appropriately.// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.letnsError=errorasNSErrorfatalError("Unresolved error\(nsError),\(nsError.userInfo)")}}}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容