SwiftUI 宣告式程式设计的前端IOS编程语言
工程结构 ContentView.swift为入口文件
Assets存放静态资源
HabeetApp为启动入口
ContentView 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import SwiftUIstruct ContentView : View { var body: some View { VStack { Image (systemName: "globe" ) .imageScale(.large) .foregroundColor(.accentColor) Text ("Hello, world!" ) } .padding() } } struct ContentView_Previews : PreviewProvider { static var previews: some View { ContentView () .previewDevice(PreviewDevice (rawValue: "iPhone 12 Pro" )) .previewDisplayName("iPhone 12 Pro" ) .previewInterfaceOrientation(.landscapeLeft) HomeView () } }
HabeetApp 1 2 3 4 5 6 7 8 9 10 11 import SwiftUI@main struct HabeetApp : App { var body: some Scene { WindowGroup { ContentView () HomeView () } } }
视图(View) @ViewBuilder可以解决优化视图之后无返回值的问题
Text 1 2 3 4 5 6 7 Text ("Stay Hungry. Stay Foolish." ) .fontWeight(.bold) .font(.title) .font(.system(size: 50 )) .minimumScaleFactor(0.7 ) .lineLimit(1 ) .foregroundColor(.indigo)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Button (role: .none, action: { }, label: { Text ("开始" ) }) .padding(.top,30 ) .foregroundColor(.white) .background(Color .purple) .cornerRadius(20 ) .buttonStyle(.borderedProminent) .buttonStyle(.plain) Button { }label: { }
Image 1 2 3 Image ("user1" ) .resizable() .scaledToFit()
Vertical Stack(VStack 垂直堆叠视图) 作用为把子视图排列成一个垂直的堆栈(默认不可见,相当于css里的display:block)
1 2 3 4 5 VStack (spacing: 20 ) { . . . }
Horizontal Stack(HStack 水平堆叠视图) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 HStack { . . . } .padding(.horizontal, 20 ) HStack (alignment: .bottom, spacing: 10 ) { . . . }
ZStack(Z轴堆叠视图) 越靠近下面的,在z轴上越高
1 2 3 4 5 6 ZStack { Text ("最底部" ) Text ("中间" ) Text ("最顶部" ) }
Rectangle Rectangle()
是 SwiftUI 中的一个视图类型,用于创建一个矩形形状的视图
注意在overlay与Rectangle等视图联系时,在overlay里使用foreach等类似语句,不会报相应的foreach里参数使用错误,而是会报错:Type ‘() -> ()’ cannot conform to ‘ShapeStyle’,会导致我们找错报错的方向(卡了半个多小时,QAQ)
1 2 3 4 Rectangle () .frame(width: 100 , height: 50 ) .foregroundColor(Color .blue)
Circle 1 2 3 Circle () .fill(Color .blue) .frame(width: 100 , height: 100 )
Spacer 「留白」(Spacer )的 SwiftUI 特殊元件,留白視圖是一個沒有內容的視圖,它在堆疊視圖中占用儘可能多的空間。例如:當你將留白視圖放置在垂直佈局中,它會在堆疊允許的範圍內垂直擴展。
1 2 Spacer () .layoutPriority(1 )
TextField 输入框
1 2 3 4 5 6 7 8 TextField ("请输入备注" , text: $textInput ) .font(Font .system(size: 16 , weight: .bold)) .padding(EdgeInsets (top: 15 , leading: 15 , bottom: 15 , trailing: 15 )) .background(Color (UIColor (red: 250 / 255 , green: 250 / 255 , blue: 255 / 255 , alpha: 1 ))) .cornerRadius(22.5 ) .textFieldStyle(PlainTextFieldStyle ())
TabView 轮播图
注意 这个currentIndex = (currentIndex + 1) % items.count里面的 items.count不能为0否则报错(卡了我半小时,主要是swiftui项目崩溃的日志实在是又长又没有重点😡)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @State private var currentIndex = 0 let timer = Timer .publish(every: 3 , on: .main, in: .common).autoconnect()let items = [ ("loginNavLogo1" , "兑换\n 商店积分" ), ("loginNavLogo2" , "发现\n 自我进步" ), ("loginNavLogo3" , "建立\n 计时标签" ), ("loginNavLogo4" , "建立\n 你的目标" ) ] TabView (selection: $currentIndex ) { ForEach (items.indices, id: \.self ) { index in VStack (spacing: 10 ) { Image (items[index].0 ) .resizable() .scaledToFit() .frame(width: 400 , height: 380 ) HStack { Text (items[index].1 ) .font(.title) .multilineTextAlignment(.leading) Spacer () } } .tag(index) } } .tabViewStyle(PageTabViewStyle (indexDisplayMode: .automatic)) .indexViewStyle(PageIndexViewStyle (backgroundDisplayMode: .always)) .onReceive(timer) {_ in print ("Timer triggered" ) currentIndex = (currentIndex + 1 ) % items.count }
1 2 3 4 5 6 7 ScrollView (.vertical, showsIndicators: false ) { ForEach (tagWithTime.indices, id: \.self ) { index in TagItemView (tagTimeIndex:tagTimeIndex) .frame(maxWidth: .infinity) } }
Picker 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 .sheet(isPresented: $showScorePicker ) { Picker ("分数" , selection: $selectedScore ) { ForEach (1 ... 8 , id: \.self ) { score in Text ("\(score) Point" ) } } .pickerStyle(WheelPickerStyle ()) .presentationDetents([.fraction(0.4 ),.medium,.large]) .edgesIgnoringSafeArea(.all) Button { showScorePicker.toggle() }label: { Text ("完成" ) .foregroundColor(Color .white) } .frame(width: 100 ,height: 40 ) .background(Color .indigo) .cornerRadius(12 ) .padding(.top,30 ) }
DatePicker 关键在于.datePickerStyle(GraphicalDatePickerStyle()),通过这个修饰符出来的样式是好看的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 .sheet(isPresented: $showDatePicker ) { VStack { DatePicker ( selection: $selectedDate , in: Date ()... ) { Text ("选择时间" ) } .datePickerStyle(GraphicalDatePickerStyle ()) .labelsHidden() .presentationDetents([.fraction(0.6 ),.large]) .edgesIgnoringSafeArea(.all) Button { showDatePicker.toggle() }label: { Text ("完成" ) .foregroundColor(Color .white) } .frame(width: 100 ,height: 40 ) .background(Color .indigo) .cornerRadius(12 ) .padding(.top,30 ) } .padding() .background(Color .white) .cornerRadius(15 ) }
视图修饰符(View Modifier) 本质上就是一个苹果为我们提供好的实例里的方法
注意每次使用视图修饰符时,SwiftUI都会在后台创建一个新的原始视图的修饰版本,所以视图修饰符的前后位置也很关键,视图修饰符不同的位置会改变其样式,同时在使用视图修饰符后返回的值也都有所不同(肯返回some View或者text),而部分视图修饰符又要求了它接受的值,所以顺序不同可能会造成类型的bug
background 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 .background { Image ("background" ) .resizable() .ignoresSafeArea() } .background( LinearGradient ( gradient: Gradient ( colors: [ Color (red: 142 / 255 , green: 150 / 255 , blue: 255 / 255 ), Color (red: 108 / 255 , green: 93 / 255 , blue: 211 / 255 ) ] ), startPoint: .trailing, endPoint: .leading ) )
padding padding设置在frame前面可以达到不改变设定宽高,只是内部改变padding
1 2 .padding(.horizontal, 20 ) .padding(.top,30 )
frame frame只是把一个视图限制在一定的宽度和高度之中,并不会改变视图本身的大小,但是可以利用子视图来撑大父视图,使其大小改变
1 2 3 .frame(width: 200 ) .frame(maxWidth: .infinity) .frame(width: 100 ,alignment: .leading)
boder 1 .border(Color .red,width: 2 )
opacity
multilineTextAlignment 文字对齐方式(主要返回的some View)
1 2 3 .multilineTextAlignment(.leading) .multilineTextAlignment(.center) .multilineTextAlignment(.trailing)
lineSpacing 行间距,增加文字行与行之间点距离
front 1 2 3 4 .font(.title) .font(.title2) .font(.title3) .font(.footnote)
不同字体在默认状态下的大小(单位为px)
kerning 文本里文字之间的间距
1 2 .kerning(2.0 ) .kerning(- 1.0 )
fontWeight 文字字重
1 2 .fontWeight(.black) .fontWeight(.bold)
alert 按钮下的方法
注意这个$alertIsVisible如果在foreeach里的视图组件使用的话,不要把它作为binding参数传进来,否则alertIsVisible为true后会同时唤出多个alert,导致alert里的参数传递会出错,应该把alertIsVisible作为视图组件的private变量,确保在foreach循环里,每个视图组件的变量alertIsVisible都私有(卡了我一晚上,本来是玩博德之门3的啊啊啊啊啊啊啊啊QAQ) 还有要注意的是,如果想在一个视图里使用多个alert,靠增加.alert的数量是没用的,应该通过返回多个Alert达到增加alert数量的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 .alert(isPresented: $isShowingAlert ) { Alert ( title: Text ("确定要删除吗?" ), message: Text ("删除后将会从您的标签移除数据" ), primaryButton: .default(Text ("确定" ), action: { deleteTag(tagName: tagName,tagTimeIndex:tagTimeIndex) }), secondaryButton: .cancel(Text ("取消" )) ) } .alert(isPresented: $time ) { if timeStop { return Alert ( title: Text ("确定要放弃吗?" ), message: Text ("本次计时将不会得到任何分数" ), primaryButton: .default(Text ("确定" ), action: { resetCountdown() time = false }), secondaryButton: .cancel(Text ("取消" )) ) } else { return Alert ( title: Text ("计时结束" ), message: Text ("本次计时获得\(tagWithTime[selectedTagIndex].tagPoint! ) Points" ), primaryButton: .default(Text ("确定" ), action: { resetCountdown() time = false }), secondaryButton: .cancel(Text ("取消" )) ) } }
animation 如果在大的动画里有部分动画不满意,可以直接在那个部分里再加一份动画,或者动画为none,也可以加上不同的id来是SwiftUI意识到这是不同的View,还有在View里加上.transition可以控制不同的动画效果,也可以用.withAnimation精确控制动画出现的时机
1 2 .animation(.easeInOut, value: ifShowTargetMenu)
controlSize 任何视图都可以使用,但是只有苹果给的预设
layoutPriority 排版的优先顺序,默认都为0
transition 1 .transition(.move(edge: .top ).combined(with: .opacity))
cornerRadius
toggle 翻转Bool变量的值
ignoresSafeArea 在进行背景颜色修改时,手机底部和顶部无法正确上色,可以使用这个Modifier
onTapGesture 在视图的顶端点击后的动作
1 2 3 .onTapGesture { ifShowMenu.toggle() }
colorMultiply 将整个图片的颜色变暗为黑色
1 2 Image ("targetBefore" ) .colorMultiply(.black)
shadow 增加阴影效果
1 .shadow(color: Color .black.opacity(0.2 ), radius: 10 , x: 0 , y: 5 )
overlay alignment:可以控制8个角的放置的位置,下面的示例固定在右下角
注意在overlay与Rectangle等视图联系时,在overlay里使用foreach等类似语句,不会报相应的foreach里参数使用错误,而是会报错:Type ‘() -> ()’ cannot conform to ‘ShapeStyle’,会导致我们找错报错的方向(卡了半个多小时,QAQ)
1 2 3 4 5 VStack { Text ("被重叠的主视图" ) }.overlay(alignment:.bottomTrailing) { Text ("覆盖的内容" ) }
onAppear 注意子组件里的onAppear在父组件是无效的,里面的内容不会执行
1 2 3 4 .onAppear { self .onDateSelected? (self .selectedDate) }
sheet 通过presentationDetents控制大小(注意presentationDetents要放在sheet里面才有效,以及该修饰符IOS16才适配)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 .sheet(isPresented: $showScorePicker ) { Picker ("分数" , selection: $selectedScore ) { ForEach (1 ... 8 , id: \.self ) { score in Text ("\(score) Point" ) } } .pickerStyle(WheelPickerStyle ()) .presentationDetents([.fraction(0.4 ),.medium,.large]) .edgesIgnoringSafeArea(.all) Button { showScorePicker.toggle() }label: { Text ("完成" ) .foregroundColor(Color .white) } .frame(width: 100 ,height: 40 ) .background(Color .indigo) .cornerRadius(12 ) .padding(.top,30 ) }
实践 毛玻璃效果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 if ifShowMenu { VisualEffectView (effect: UIBlurEffect (style: .light)) .ignoresSafeArea() Color .white.opacity(0.3 ) .ignoresSafeArea() } struct VisualEffectView : UIViewRepresentable { var effect: UIVisualEffect ? func makeUIView (context : UIViewRepresentableContext <Self >) -> UIVisualEffectView { UIVisualEffectView () } func updateUIView (_ uiView : UIVisualEffectView , context : UIViewRepresentableContext <Self >) { uiView.effect = effect } }
视图宽度固定为设备的一半,并置于左侧 难点在于直接使用frame固定尺寸的话,是达不到置于左侧的效果的
同时下面的例子还使得图片固定在左上角
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 if ifShowMenu { DrawerMenu (isDrawerOpen: $ifShowMenu ) } struct DrawerMenu : View { @Binding var isDrawerOpen: Bool var body: some View { GeometryReader { geometry in VStack (alignment: .leading) { Image ("Avatar" ) .resizable() .scaledToFit() .frame(width: 40 , height: 40 ) .alignmentGuide(HorizontalAlignment .leading) { _ in geometry.size.width / 2 } .padding(.leading,30 ).padding(.bottom,10 ) Spacer () } .frame(width: geometry.size.width / 2 , height: geometry.size.height+ 40 ) .background(Color .white) } } }
两种导航方式 NavigationStack 这种方法会在左上角留下back的返回字样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 NavigationStack { ZStack { Button { ifShowTarget= true }label: { HStack { Image ("targetBefore" ) .resizable() .scaledToFit() .frame(width: 30 , height: 30 ) .padding(.leading,5 ) Text ("目标" ) .frame(width: 120 ,alignment: .leading) } } NavigationLink ("" , destination: TargetView (), isActive: $ifShowTarget ) } } NavigationStack { ZStack { NavigationLink (destination: TargetView ()){ Text ("点击跳转" ) } } }
fullScreenCover 这种方法是在当前页面直接开一个新的视图,比较符合常规的导航
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Button { ifShowTarget= true }label: { HStack { Image ("targetBefore" ) .resizable() .scaledToFit() .frame(width: 30 , height: 30 ) .padding(.leading,5 ) Text ("目标" ) .frame(width: 120 ,alignment: .leading) } } if ifShowTarget{ NullView () .fullScreenCover(isPresented: $ifShowTarget , content: { TargetView () }) }
API网络请求 POST请求 第一步是建立好结构体接收传回来的参数,可以通过postman获取到传回来的JSON数据,根据数据进行书写
第二步建立urlRequest,并给出链接的设定,包括请求方式(httpMethod),请求头(header),请求参数(httpBody),其中请求参数这里需要转化为Data类型的数据,如果是直接传入String则使用userEmail.data(using: .utf8)转化,如果是json数据则进行转化:
1、let requestData = [“userEmail”: userEmail]
2、let jsonData = try JSONSerialization.data(withJSONObject: requestData)
第三步是创建 URLSession 数据任务,传回来的数据也要通过解码:
JSONDecoder().decode(ResponseData.self, from: data) 最后还有处理各种出错情况
注意下面的例子只是将类型粗糙的分类,具体怎么请求最主要要是要看后端的api的需求
例子1 在大体上该例子常用于获取数组类型的数据
JSON格式 该格式里的data里最外围是[ ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 { "code" : "00000" , "message" : "一切 ok" , "data" : [ { "id" : null , "userId" : null , "userEmail" : null , "targetName" : "测试1" , "targetDescribe" : "hhh" , "targetColor" : null , "targetPoint" : "3" , "deadline" : null , "status" : "0" , "deadlineString" : null , "ifPoints" : null , "ifTargetNull" : null , "ifTargetUpdate" : null , "targetId" : "1692785063700615169" } , { "id" : null , "userId" : null , "userEmail" : null , "targetName" : "测试2" , "targetDescribe" : "hhh" , "targetColor" : null , "targetPoint" : "7" , "deadline" : null , "status" : "0" , "deadlineString" : null , "ifPoints" : null , "ifTargetNull" : null , "ifTargetUpdate" : null , "targetId" : "1692785123469447170" } ] , "ok" : true }
代码 在细节上该例子讲参数写死,并返回值,且传给后端的数据为text( let jsonData = userEmail.data(using: .utf8) )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 import Foundationstruct ResponseData : Decodable { let data: [TagWithTime ] } struct TagWithTime : Decodable , Identifiable { let id: String let tagName: String let tagDescribe: String let tagHour: String let tagMinute: String let tagPoint: String } class TagDataManager { func fetchTagData (completion : @escaping ([TagWithTime ]? , Error ?) -> Void ) { guard let url = URL (string: "https://tengenchang.top/tag/get" ) else { completion(nil , nil ) return } let userEmail = "3489044730@qq.com" do { let jsonData = userEmail.data(using: .utf8) var request = URLRequest (url: url) request.httpMethod = "POST" request.setValue("application/json" , forHTTPHeaderField: "Content-Type" ) request.httpBody = jsonData URLSession .shared.dataTask(with: request) { data, response, error in if let data = data { do { let decodedResponse = try JSONDecoder ().decode(ResponseData .self , from: data) completion(decodedResponse.data, nil ) } catch { print ("JSON decoding error: \(error) " ) completion(nil , error) } } else if let error = error { completion(nil , error) } }.resume() } catch { completion(nil , error) } } } import SwiftUIstruct TagView : View { @State private var ifShowMenu:Bool = false @State private var ifShowTarget:Bool = false @State private var showWhichView:Int = 2 @State private var ifDelete:Bool = false @State private var tagWithTime: [TagWithTime ] = [] @State private var ifshowTagDetailBNull = false let tagDataManager = TagDataManager () var body: some View { ZStack { VStack { NavView (ifShowMenu: $ifShowMenu ,showWhichView:$showWhichView ,ifDelete:$ifDelete ) ForEach (tagWithTime, id: \.id) { tag in TagItemView (ifDelete:$ifDelete ,isShowingAlert:$isShowingAlert ,tagName:tag.tagName,tagDescribe:tag.tagDescribe) } Spacer () } }.onAppear { fetchTagData() } } func fetchTagData () { tagDataManager.fetchTagData { fetchedData, error in if let fetchedData = fetchedData { ifshowTagDetailBNull = fetchedData.isEmpty tagWithTime = fetchedData } else { print ("Error fetching data: \(error? .localizedDescription ?? "Unknown error" ) " ) } } } }
例子2 在大体上该例子常用于删除数据
JSON格式 该格式就是后端返回的值全是null
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { "code" : "00000" , "message" : "一切 ok" , "data" : { "id" : null, "userId" : null, "picUrl" : null, "tagName" : null, "tagDescribe" : null, "tagColor" : null, "tagPoint" : null, "tagHour" : null, "tagMinute" : null, "creatTime" : null, "userEmail" : null, "ifRepeat" : null, "ifTagNull" : null, "ifTagUpdate" : null, "tagId" : null }, "ok" : true }
代码 在细节上该例子不返回任何参数,这里data明明由{ }包裹,但是却使用[TagWithTime]类型解密,还没有报错的原因在于,该方法的并不需要访问里面的数据,completion也不返回[TagWithTime],,所以可以这么使用,不能使用[TagWithTime]类型解密的例子为例子4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 import Foundationstruct TagResponseData : Decodable { let data: [TagWithTime ] } struct TagWithTime : Decodable , Identifiable { let id: String ? let tagName: String ? let tagDescribe: String ? let tagHour: String ? let tagMinute: String ? let tagPoint: String ? let ifTagNull:String ? } class TagDataManager { func deleteTag (tagName : String , completion : @escaping (Error ?) -> Void ) { guard let url = URL (string: "https://tengenchang.top/tag/delete" ) else { completion(nil ) return } let tagName = tagName let jsonData = tagName.data(using: .utf8) var request = URLRequest (url: url) request.httpMethod = "POST" request.setValue("application/json" , forHTTPHeaderField: "Content-Type" ) request.httpBody = jsonData URLSession .shared.dataTask(with: request) { _ , _ , error in if let error = error { completion(error) } else { completion(nil ) } }.resume() } } import SwiftUIstruct TagItemView : View { let tagDataManager = TagDataManager () @Binding var tagWithTime:[TagWithTime ] let tagTimeIndex:Int var body: some View { HStack { if ifDelete{ Button { isShowingAlert = true }label: { Image ("x" ) .resizable() .scaledToFit() .frame(width: 12 ,height: 12 ) }.alert(isPresented: $isShowingAlert ) { Alert ( title: Text ("确定要删除吗?" ), message: Text ("删除后将会从您的标签移除数据" ), primaryButton: .default(Text ("确定" ), action: { deleteTag(tagName: tagName,tagTimeIndex:tagTimeIndex) }), secondaryButton: .cancel(Text ("取消" )) ) } }else { } } } func deleteTag (tagName :String ,tagTimeIndex :Int ){ tagDataManager.deleteTag(tagName: tagName) { error in if error == nil { deleteSuccess= true tagNum-= 1 if tagNum== 0 { ifshowTagDetailBNull= true } tagWithTime.remove(at: tagTimeIndex) print ("删除标签成功" ) } else { print ("删除标签失败" ) } } } }
例子3 在大体上该例子用于检验用户是否注册,利用completion返回Int,方便用户直接跳转到登录界面或者注册界面
JSON格式 该格式就是后端的代码就是return null
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { "code" : "00000" , "message" : "一切 ok" , "data" : { "userId" : null , "userName" : null , "picData" : null , "picUrl" : null , "userEmail" : "3489044730@qq.com" , "userPassword" : null , "userCode" : null , "completeTarget" : null , "point" : null , "ifUpdate" : null , "openId" : null } , "ok" : true }
代码 在细节上该例子传入的参数是动态的,返回简单参数如(Int),利用completion返回起来简单,且使用时直接调用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 import Foundationstruct UserResponseData : Decodable { let code: String ? } class UserDataManager { func checkEmail (email : String , completion : @escaping (Int ) -> Void ) { guard let url = URL (string: "https://tengenchang.top/user/home" ) else { completion(12 ) return } let userEmail = email let jsonData = userEmail.data(using: .utf8) var request = URLRequest (url: url) request.httpMethod = "POST" request.setValue("application/json" , forHTTPHeaderField: "Content-Type" ) request.httpBody = jsonData URLSession .shared.dataTask(with: request) { data, response, error in if let data = data { do { let decodedData = try JSONDecoder ().decode(UserResponseData .self , from: data) if let code = decodedData.code { DispatchQueue .main.async { if code == "nil" { completion(12 ) } else { completion(11 ) } } } } catch { print (error) completion(12 ) } } }.resume() } } import SwiftUIstruct HomeView : View { @EnvironmentObject private var userData: UserData let userDataManager = UserDataManager () var body: some View { VStack (alignment: .leading,spacing: 20 ){ Button { let isValidQQEmail = isValidQQEmailFormat(email: userData.userEmail) if isValidQQEmail{ userDataManager.checkEmail(email: userData.userEmail) { retrurnShowWhichView in showWhichView = retrurnShowWhichView timerTriggered = true } }else { ifshowTextAlert= true } }label: { HStack (){ Text ("继续" ) } } }.padding(15 ) .overlay { if ifshowTextAlert{ TextAlertView (textContant:$textContant ,ifshowTextAlert:$ifshowTextAlert ) } } } }
例子4 大体上该例子作用于获取该用户的账户信息,一般不是数组
JSON格式 注意json返回的数据不是数组,且要获取里面的数据时时,不要为结构体或者类加上[]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { "code" : "00000" , "message" : "一切 ok" , "data" : { "id" : null, "userId" : null, "point" : null, "pointType" : null, "pointName" : null, "pointDescribe" : null, "pointDate" : null, "userEmail" : "3489044730@qq.com" , "userTimeP" : "过去一天" , "pointAll" : "0" , "progress" : "0" , "pointInsistence" : "0" , "pointAverage" : "0.0" , "completeTarget" : "0" , "completeTargetRate" : "0.0" , "ifProgress" : null }, "ok" : true }
代码 在细节上该例子解析出来的数据放在类里面,而不是数组,注意拆包和初始化时候的使用可选项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 import Foundationstruct PointRecordResponseData : Decodable { let data: PointRecordData } class PointRecordData :Decodable { let userTimeP, pointAll, progress: String let pointInsistence, pointAverage, completeTarget, completeTargetRate: String } class UserDataManager { func fetchPointRecordData (userTimeP :String ,completion : @escaping (PointRecordData ?, Error ?) -> Void ) { guard let url = URL (string: "https://tengenchang.top/pointRecord/get" ) else { completion(nil , nil ) return } let userEmail = "3489044730@qq.com" let userTimeP= userTimeP let parameters: [String : Any ] = ["userEmail" : userEmail, "userTimeP" : userTimeP] let jsonData = try? JSONSerialization .data(withJSONObject: parameters) var request = URLRequest (url: url) request.httpMethod = "POST" request.setValue("application/json" , forHTTPHeaderField: "Content-Type" ) request.httpBody = jsonData URLSession .shared.dataTask(with: request) { data, response, error in if let data = data { do { let decodedResponse = try JSONDecoder ().decode(PointRecordResponseData .self , from: data) completion(decodedResponse.data, nil ) } catch { print ("JSON decoding error: \(error) " ) completion(nil , error) } } else if let error = error { completion(nil , error) } }.resume() } } import SwiftUIstruct UserView : View { let userDataManager = UserDataManager () @State private var userTimeP:String = "过去一周" @State private var pointRecordData:PointRecordData ? let pointAll:String = "10" var body: some View { ZStack { VStack { ZStack { VStack (alignment: .leading){ Text ("获取分数" ) HStack { Text ((pointRecordData? .pointAll) ?? "0" ) } Text ("努力的\(String(userTimeP.suffix(2 ))) !" ) } } ZStack { VStack (alignment: .leading){ Text ("进步" ) HStack { Text ((pointRecordData? .progress) ?? "0" ) } } } HStack { VStack { VStack (alignment: .leading){ HStack { Text ("\((pointRecordData? .pointInsistence) ?? "0" ) \n 连续得分" ) } } VStack (alignment: .leading){ HStack { VStack { Text ("\((pointRecordData? .completeTargetRate) ?? "0.0" ) %" ) } } } } VStack { VStack (alignment: .leading){ HStack { VStack { Text ((pointRecordData? .pointAverage) ?? "0.0" ) } } } VStack (alignment: .leading){ HStack { VStack { Text ("\((pointRecordData? .completeTarget) ?? "0" ) 个目标" ) } } } } } } if ifShowUserMenu{ if userTimeP== "过去一周" { VStack { Button { userTimeP= "过去一天" fetchPointRecordData() }label: { Text ("过去一天" ) } Button { userTimeP= "过去一月" fetchPointRecordData() }label: { Text ("过去一月" ) } } } } } } func fetchPointRecordData () { userDataManager.fetchPointRecordData(userTimeP: userTimeP) { fetchedData, error in if let fetchedData = fetchedData { pointRecordData= fetchedData print ((pointRecordData? .pointAll)! ) } else { print ("Error fetching data: \(error? .localizedDescription ?? "Unknown error" ) " ) } } } }
自定义文字弹窗提示 因为swiftui中只有alert,且这个视图在官方的规定下是必定要有按钮的,所以为了满足项目的需求,我进行了自定义的文字弹窗提示,该弹窗还会在几秒后自动消失
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 struct TextAlertView : View { @State private var isTextVisible = true @Binding var textContant:String @Binding var ifshowTextAlert:Bool var body: some View { VStack { if isTextVisible { Text (textContant) .foregroundColor(Color .white) .padding([.top,.bottom],10 ) .padding([.leading,.trailing],15 ) .background(Color .secondary) .cornerRadius(10 ) .onAppear { DispatchQueue .main.asyncAfter(deadline: .now() + 0.6 ) { withAnimation { isTextVisible = false } ifshowTextAlert= false } } } } } }
foreach两种方式获得index 1、利用Array包装数组获取到index,该方法获取元素使用类似target.targetName的方式
1 2 3 4 5 6 7 8 9 ForEach (Array (targetNoTime.enumerated()), id: \.element.id) { (index, target) in TargetItemView (targetName: target.targetName! , targetDescribe: target.targetDescribe! , targetId: target.targetId! , targetPoint:target.targetPoint! , targetStatus: target.status! , targetTimeIndex:index ) }
2、使用数组方法中的.indices,获取到index,该方法获取元素使用类似tagWithTime[index].tagName的方式
1 2 3 4 5 6 ForEach (tagWithTime.indices, id: \.self ) { index in TagItemView (tagName: tagWithTime[index].tagName! , tagDescribe:tagWithTime[index].tagDescribe! , tagWithTime:$tagWithTime , tagTimeIndex:index) }
自定义横向日期显示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 import SwiftUIstruct TargetNav1View : View { private let calendar = Calendar .current private let dateFormatter: DateFormatter = { let formatter = DateFormatter () formatter.dateFormat = "E" return formatter }() @State private var selectedDayIndex = 0 var body: some View { ScrollView (.horizontal, showsIndicators: false ) { HStack (spacing: 18 ) { ForEach (0 ..< 30 ) { index in VStack { Text (self .dayName(for: index)) .foregroundColor(self .selectedDayIndex == index ? .black : .secondary) .font(.system(size: 12 )) Text (self .dayNumber(for: index)) .frame(width: 36 , height: 36 ) .foregroundColor(Color (rgba: (207 , 200 , 255 , 1 ))) .background( LinearGradient ( gradient: Gradient ( colors: self .gradientColors(for: index) ), startPoint: .top, endPoint: .bottom ) ) .cornerRadius(22.5 ) .onTapGesture { self .selectedDayIndex = index } } .cornerRadius(22.5 ) } } .padding(.top, 20 ) }.padding([.leading,.trailing],20 ) } private func dayName (for index : Int ) -> String { let currentDate = calendar.date(byAdding: .day, value: index, to: Date ()) ?? Date () return dateFormatter.string(from: currentDate) } private func dayNumber (for index : Int ) -> String { let currentDate = calendar.date(byAdding: .day, value: index, to: Date ()) ?? Date () let day = calendar.component(.day, from: currentDate) return "\(day) " } private func gradientColors (for index : Int ) -> [Color ] { if self .selectedDayIndex == index { return [ Color (red: 142 / 255 , green: 150 / 255 , blue: 255 / 255 ), Color (red: 108 / 255 , green: 93 / 255 , blue: 211 / 255 ) ] } else { return [ Color .clear, Color .clear ] } } }
自定义日期比较器 难点有两个,第一点是仅关注日期部分,忽略时间的影响,解决方法是把传进来的时间设置为0小时、0分钟、0秒,来忽略小时和分钟对日期差的干扰,并且用calendar.dateComponents(_:from:to:)来计算目标日期和选定日期的月份和日子的差异,第二点在于进入目标页面时间差的初值问题,解决方法是在获取数据时,直接调用求时间差的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 import SwiftUIstruct TargetNav1View : View { let selectedDate: Date = Date () private let calendar = Calendar .current private let dateFormatter: DateFormatter = { let formatter = DateFormatter () formatter.dateFormat = "E" return formatter }() @State private var selectedDayIndex = 0 var onDateSelected: ((Date ) -> Void )? var body: some View { ScrollView (.horizontal, showsIndicators: false ) { HStack (spacing: 18 ) { ForEach (0 ..< 30 ) { index in VStack { Text (self .dayName(for: index)) .foregroundColor( self .selectedDayIndex == index ? .black : .secondary ) .font(.system(size: 12 )) Text (self .dayNumber(for: index)) .frame(width: 36 , height: 36 ) .foregroundColor(Color (rgba: (207 , 200 , 255 , 1 ))) .background( LinearGradient ( gradient: Gradient ( colors: self .gradientColors(for: index) ), startPoint: .top, endPoint: .bottom ) ) .cornerRadius(22.5 ) .onTapGesture { self .selectedDayIndex = index if let selectedDate = self .calendar.date(byAdding: .day, value: index, to: Date ()) { self .onDateSelected? (selectedDate) } } } .cornerRadius(22.5 ) } } .padding(.top, 20 ) }.padding([.leading,.trailing],20 ) } private func dayName (for index : Int ) -> String { let currentDate = calendar.date(byAdding: .day, value: index, to: Date ()) ?? Date () return dateFormatter.string(from: currentDate) } private func dayNumber (for index : Int ) -> String { let currentDate = calendar.date(byAdding: .day, value: index, to: Date ()) ?? Date () let day = calendar.component(.day, from: currentDate) return "\(day) " } private func gradientColors (for index : Int ) -> [Color ] { if self .selectedDayIndex == index { return [ Color (red: 142 / 255 , green: 150 / 255 , blue: 255 / 255 ), Color (red: 108 / 255 , green: 93 / 255 , blue: 211 / 255 ) ] } else { return [ Color .clear, Color .clear ] } } } TargetNav1View (onDateSelected: { selectedDate in for _ in 0 ..< targetWithTime.count { targetDateInfo.append(TargetDateInfo (dayDifference: 0 , timeString: "" )) } let calendar = Calendar .current for index in 0 ..< self .targetWithTime.count { print (index) let dateFormatter = DateFormatter () dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" if let deadlineDate = dateFormatter.date(from: self .targetWithTime[index].deadline! ), let startDate = calendar.date(bySettingHour: 0 , minute: 0 , second: 0 , of: selectedDate) { print ("startDate:" ,startDate) let deadlineComponents = calendar.dateComponents([.month, .day], from: deadlineDate) let selectedComponents = calendar.dateComponents([.month, .day], from: startDate) if let dayDifference = calendar.dateComponents([.day], from: selectedComponents, to: deadlineComponents).day { self .dayDifference = dayDifference print ("index:" ,index) targetDateInfo[index].dayDifference = dayDifference targetDateInfo[index].timeString = "" if dayDifference < 0 { let monthDayFormatter = DateFormatter () monthDayFormatter.dateFormat = "MM.dd" let monthDayString = monthDayFormatter.string(from: deadlineDate) self .monthDayString = monthDayString targetDateInfo[index].timeString = monthDayString print ("Month and Day: \(monthDayString) " ) } else if dayDifference == 0 { let timeFormatter = DateFormatter () timeFormatter.dateFormat = "HH:mm" let timeString = timeFormatter.string(from: deadlineDate) self .timeString = timeString targetDateInfo[index].timeString = timeString print ("Time: \(timeString) " ) } else if dayDifference > 0 { print ("Day difference: \(dayDifference) " ) } } } } })
三种Picker 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 .sheet(isPresented: $showScorePicker ) { Picker ("分数" , selection: $selectedScore ) { ForEach (1 ... 8 , id: \.self ) { score in Text ("\(score) Point" ) } } .pickerStyle(WheelPickerStyle ()) .presentationDetents([.fraction(0.4 ),.medium,.large]) .edgesIgnoringSafeArea(.all) Button { showScorePicker.toggle() }label: { Text ("完成" ) .foregroundColor(Color .white) } .frame(width: 100 ,height: 40 ) .background(Color .indigo) .cornerRadius(12 ) .padding(.top,30 ) } .sheet(isPresented: $showDatePicker ) { VStack { DatePicker ( selection: $selectedDate , in: Date ()... ) { Text ("选择时间" ) } .datePickerStyle(GraphicalDatePickerStyle ()) .labelsHidden() .presentationDetents([.fraction(0.6 ), .large]) .edgesIgnoringSafeArea(.all) Button { showDatePicker.toggle() deadlineString = dateFormatter.string(from: selectedDate) } label: { Text ("完成" ) .foregroundColor(Color .white) } .frame(width: 100 , height: 40 ) .background(Color .indigo) .cornerRadius(12 ) .padding(.top, 30 ) } .padding() .background(Color .white) .cornerRadius(15 ) } .sheet(isPresented: $showDatePicker ) { VStack { DatePicker ( selection: $selectedDate , in: Date ()... ) { Text ("选择时间" ) } .datePickerStyle(GraphicalDatePickerStyle ()) .labelsHidden() .presentationDetents([.fraction(0.6 ),.large]) .edgesIgnoringSafeArea(.all) Button { showDatePicker.toggle() }label: { Text ("完成" ) .foregroundColor(Color .white) } .frame(width: 100 ,height: 40 ) .background(Color .indigo) .cornerRadius(12 ) .padding(.top,30 ) } .padding() .background(Color .white) .cornerRadius(15 ) }
崩溃总结 因为swiftui的崩溃日志实在是难以读懂,所以根据经验总结了一下崩溃的原因
首先是数组越界问题,比如说常见的remove之后,还在用index去访问数组的元素,就会导致数组越界,所以看来还是避免使用index下标去访问数组元素,还要注意获取数据和使用数据的先后
其次是拆包问题,常见于处理后端数据的时候,后端传进来的数据可能为空,但是我们并没有使用可选型去接收这个参数,就会导致崩溃 的产生
点击更换样式 利用onTapGesture通过点击更换index,再使用三元运算符判断更改样式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 struct TargetNav1View : View { let selectedDate: Date = Date () private let calendar = Calendar .current private let dateFormatter: DateFormatter = { let formatter = DateFormatter () formatter.dateFormat = "E" return formatter }() @State private var selectedDayIndex = 0 var onDateSelected: ((Date ) -> Void )? var body: some View { ScrollView (.horizontal, showsIndicators: false ) { HStack (spacing: 18 ) { ForEach (0 ..< 30 ) { index in VStack { Text (self .dayName(for: index)) .foregroundColor( self .selectedDayIndex == index ? .black : .secondary ) .font(.system(size: 12 )) Text (self .dayNumber(for: index)) .frame(width: 36 , height: 36 ) .foregroundColor(Color (rgba: (207 , 200 , 255 , 1 ))) .background( LinearGradient ( gradient: Gradient ( colors: self .gradientColors(for: index) ), startPoint: .top, endPoint: .bottom ) ) .cornerRadius(22.5 ) .onTapGesture { self .selectedDayIndex = index if let selectedDate = self .calendar.date(byAdding: .day, value: index, to: Date ()) { self .onDateSelected? (selectedDate) } } } .cornerRadius(22.5 ) } } .padding(.top, 20 ) }.padding([.leading,.trailing],20 ) } private func dayName (for index : Int ) -> String { let currentDate = calendar.date(byAdding: .day, value: index, to: Date ()) ?? Date () return dateFormatter.string(from: currentDate) } private func dayNumber (for index : Int ) -> String { let currentDate = calendar.date(byAdding: .day, value: index, to: Date ()) ?? Date () let day = calendar.component(.day, from: currentDate) return "\(day) " } private func gradientColors (for index : Int ) -> [Color ] { if self .selectedDayIndex == index { return [ Color (red: 142 / 255 , green: 150 / 255 , blue: 255 / 255 ), Color (red: 108 / 255 , green: 93 / 255 , blue: 211 / 255 ) ] } else { return [ Color .clear, Color .clear ] } } }
获取到数据后再显示页面 这个例子运用的场景,在页面一加载就需要显示的数据,那么这个时候异步执行的网络请求方法,还没有获取到数据,这时候视图上获取数据就会获取不到,或者直接数据越界(因为我们一般数据设置为空数组[ ]),
有三种方法解决,第一种比如说为数据赋初值,这样项目就不会崩溃了,但是就像前面说的这时候还没有获取到数据,那么页面就会一闪而过一个奇怪的数据,
所以使用第二种方法,在异步执行的网络请求方法完成后,在方法里赋值isDataLoaded代表数据加载完成,并把isDataLoaded作为if的条件,为true再显示页面,虽然其实页面会显示的慢一点,但是这点时间用户看不出来,是比较好的选择,
还有第三种方法,比如说之前我在微信小程序里,通过在该页面的前一个页面,提前获取到数据,然后再传递给该页面,就可以到达一样的效果,唯一比较麻烦的是一般这样出现在登录界面,就需要进行多种数据的获取,会导致一定的卡顿
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 if isDataLoaded{ } func fetchTagData () { tagDataManager.fetchTagData { fetchedData, error in if let fetchedData = fetchedData { tagWithTime = fetchedData tagNum= tagWithTime.count if tagWithTime[0 ].ifTagNull== "1" { ifshowTagDetailBNull= true } print (tagWithTime) isDataLoaded = true } else { print ("Error fetching data: \(error? .localizedDescription ?? "Unknown error" ) " ) } } }
自定义倒计时器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func startCountdown () { let hoursInSeconds = (Int (tagWithTime[selectedTagIndex].tagHour! ) ?? 1 ) * 3600 let minutesInSeconds = (Int (tagWithTime[selectedTagIndex].tagMinute! ) ?? 30 ) * 60 remainingTimeInSeconds = TimeInterval (hoursInSeconds + minutesInSeconds) countdownTimer = Timer .scheduledTimer(withTimeInterval: 1 , repeats: true ) { timer in if remainingTimeInSeconds > 0 { remainingTimeInSeconds -= 1 } else { timer.invalidate() time= true timeStop= false finishTag(tagName: tagWithTime[selectedTagIndex].tagName! ) userData.point+= Int (tagWithTime[selectedTagIndex].tagPoint! )! } } }
swiftui基础 数组 可以通过[结构体]的方式,来定义数组
注意在通过targetDateInfo[index].dayDifference去添加数组的时候,需要先初始化好targetDateInfo,不然index会使targetDateInfo越界,之前在Vue里面经常直接使用push的方法,所以忘记了这点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 struct TargetDateInfo { var dayDifference: Int var timeString: String } @State private var targetDateInfo:[TargetDateInfo ]= []for _ in 0 ..< targetWithTime.count { targetDateInfo.append(TargetDateInfo (dayDifference: 0 , timeString: "" )) } for index in 0 ..< self .targetWithTime.count { let dateFormatter = DateFormatter () dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" if let deadlineDate = dateFormatter.date(from: self .targetWithTime[index].deadline! ), let startDate = Calendar .current.date(bySettingHour: 0 , minute: 0 , second: 0 , of: selectedDate) { print ("startDate:" ,startDate) let calendar = Calendar .current let components = calendar.dateComponents([.day], from: startDate, to: deadlineDate) if let dayDifference = components.day { self .dayDifference= dayDifference print ("index:" ,index) targetDateInfo[index].dayDifference= dayDifference targetDateInfo[index].timeString= "" if dayDifference <= 0 { let timeFormatter = DateFormatter () timeFormatter.dateFormat = "HH:mm" let timeString = timeFormatter.string(from: deadlineDate) self .timeString= timeString targetDateInfo[index].timeString= timeString print ("Time: \(timeString) " ) } else { print ("Day difference: \(dayDifference) " ) } } } }
状态(@State、@StateObject) @StateObject在跟踪类/结构体内的属性变化时使用
SwiftUI的状态能够使body在状态改变时重新渲染(相当于Vue里)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct ContentView : View { @State private var alertIsVisible:Bool = false var body: some View { VStack { Button (action: { self .alertIsVisible= true }) { Text ("点我" ) } .alert(isPresented: $alertIsVisible , content: { return Alert (title: Text ("你好" ),message: Text ("这是弹窗" ),dismissButton: . default (Text ("好好好 " ))) }) } .padding() } }
绑定(@Binding) 其实就是绑定传入的值
1 2 3 4 5 6 7 8 DrawerMenu (isDrawerOpen: $ifShowMenu , ifShowTarget: $ifShowTarget )struct DrawerMenu : View { @Binding var isDrawerOpen: Bool @Binding var ifShowTarget:Bool var body: some View { } }
全局变量(@EnvironmentObject) 定义一个实现ObservableObject协议的类,需要跟踪的数值使用@Published 修饰
1 2 3 4 class UserData : ObservableObject { @Published var userEmail: String = "" }
通过.envrionmentObject(对象)将一个对象放置到环境中
1 2 3 4 5 6 7 8 9 10 11 12 @main struct HabeetApp : App { @StateObject private var userData = UserData () var body: some Scene { WindowGroup { ContentView () .environmentObject(userData) } } }
在view中使用这个对象,@EnvironmentObject修饰对象
1 2 3 4 5 6 7 8 9 struct HomeView : View { @EnvironmentObject private var userData: UserData var body: some View { TextField ("请输入邮箱" , text: $userData .userEmail) } }
注意,如果预览没有.envrionmentObject()设置环境中的对象,程序就会崩溃,包括导航到需要用的视图
1 2 3 4 5 6 7 8 9 10 11 12 struct ContentView_Previews : PreviewProvider { static var previews: some View { ContentView ().environmentObject(UserData ()) } } struct HomeView_Previews : PreviewProvider { static var previews: some View { HomeView ().environmentObject(UserData ()) } }
单元测试(Unit Text) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import XCTest@testable import Habeetfinal class HabeetTests : XCTestCase { var game:Game ! override func setUpWithError () throws { game= Game () } override func tearDownWithError () throws { game= nil } func testExample () throws { XCTAssertEqual (game.points(sliderValue: 50 ), 999 ) } func testPerformanceExample () throws { self .measure { } } }
项目结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ProjectRoot ├── Views │ ├── ContentView.swift │ ├── MainContent.swift │ ├── DrawerMenu.swift │ ├── TargetView.swift │ └── ...other view files ├── Extensions │ ├── Color+ Extensions.swift │ ├── View + Extensions.swift ├── └── ...other extension files ├── Helpers │ ├── Constants.swift │ └── ...other helper files └── Models ├── Target.swift └── ...other model files
快捷键 建立新的 Swift 文件:command+N
将视图包含进 VStack、HStack、ZStack:选中视图+command(Embed in xxx)
要將存放代码的 VStack、HStack、ZStack 提取出来(新的stuck):选中视图+command (Extract Subview)
插入不同视图:command+Shift+L(选中后可以用鼠标拖拽到不同位置,同时不同位置也可以达成自动创建Stack的效果)
移动代码到 上一行/下一行:option+command+[ / ]
实机调试:command+R
刷新预览:option+command+P
查看视图内属性的详情:option+点击
只构建项目不调试:command+B
进行单元测试:command+U