项目背景
又到了吃饭的时刻了,翻开一些餐饮App
翻来翻去都不知道想吃什么,感觉全部都吃过了,看到都有点儿腻。
有没有一个App
能够帮我随机引荐吃什么的呢?想了想,爽性我自己写一个吧!
说干就干。
全文约3500字,估计阅览时长为5分钟,实操时长约15分钟。
项目建立
首要,创立一个新的SwiftUI
项目,命名为MyMenu
。
Model部分
数据模型
首要是数据部分的预备,咱们创立一个新的Swift
文件,命名为Model.swift
。
import SwiftUI
class Model: Decodable {
var foodTime: String
var foodName: String
var foodImageURL: String
}
上述代码中,咱们创立了一个Model
类,遵从Decodable
协议。
Decodable
协议能够协助咱们解析来自网络恳求中的Json
数据格式,咱们声明晰3个String
类型的变量:餐段foodTime
、食物称号foodName
、食物图片foodImageURL
。
回到ContentView
文件,运用@State
修饰符声明一个数组存在Model
数据,示例:
@State var models: [Model] = []
Json数据
数据源部分,咱们运用第三方网站工具,生成Json
数据,示例:
咱们拿到了Json
数据的地址,咱们也在ContentView
文件中声明,示例:
let DataURL = "https://api.npoint.io/4e97acfc3e5f73300779"
这样咱们就完结了基础的数据预备。
View部分
色彩拓宽
为了更好地运用16进制色彩值,咱们对Color
进行拓宽。创立一个新的Swift
文件,命名为ColorHexString
。
import SwiftUI
extension Color {
static func rgb(_ red: CGFloat, green: CGFloat, blue: CGFloat) -> Color {
return Color(red: red / 255, green: green / 255, blue: blue / 255)
}
static func Hex(_ hex: UInt) -> Color {
let r: CGFloat = CGFloat((hex & 0xFF0000) >> 16)
let g: CGFloat = CGFloat((hex & 0x00FF00) >> 8)
let b: CGFloat = CGFloat(hex & 0x0000FF)
return rgb(r, green: g, blue: b)
}
}
这样咱们就能够在接下来的View
页面款式中直接运用16进制色彩值了。
标题
先声明一个变量存储当时餐段信息,后边咱们会经过当时时刻来判别现在归于哪一个餐段。示例:
var DefaultTime:String = "午饭"
然后咱们构建一个标题视图,并在ContentView
视图中展现。示例:
// 标题
func TitleView(time:String) -> some View {
HStack {
Text("当时餐段 : "+time)
.font(.title2)
.fontWeight(.bold)
Spacer()
Image(systemName: "rectangle.grid.1x2.fill")
.foregroundColor(Color.Hex(0x67C23A))
}
.padding(.horizontal)
.padding(.top)
}
上述代码中,咱们界说了一个TitleView
办法,传入标题参数,回来View
视图。
咱们运用Text
作为标题,运用String
字符串拼接办法展现,另外运用Image
构建了一个切换餐段的图标,之后的交互中会运用。
引荐成果
引荐成果部分由餐品图片和餐品称号组成,咱们也声明2个变量存储它,示例:
var DefaultImageURL:String = "https://img0.baidu.com/it/u=156558209,1663147989&fm=253&fmt=auto&app=138&f=JPEG?w=626&h=500"
var DefaultName:String = "今日想吃点啥?"
引荐成果款式部分,咱们采用最简单的纵向布局进行组合,示例:
//引荐成果
func CardView(imageURL: String, name: String) -> some View {
VStack {
AsyncImage(url: URL(string: imageURL))
.aspectRatio(contentMode: .fit)
.frame(minWidth: 120, maxWidth: .infinity, minHeight: 120, maxHeight: .infinity)
Text(name)
.font(.system(size: 17))
.fontWeight(.bold)
.foregroundColor(.black)
.padding()
}
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.Hex(0x67C23A), lineWidth: 2))
.padding([.top, .horizontal])
}
上述代码中,咱们界说了一个办法CardView
,传入imageURL
、name
,回来一个View
视图。
在CardView
视图中,咱们运用AsyncImage
来创立餐品图片,然后运用Text
来展现餐品称号,而且给整个视图overlay
加了边框线。
引荐按钮
同样的办法,咱们创立一个引荐按钮,用于随机挑选餐品,先完结款式部分,示例:
//引荐按钮
func ChooseBtn() -> some View {
Button(action: {
}) {
Text("一键引荐")
.font(.system(size: 17))
.fontWeight(.bold)
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.foregroundColor(.white)
.background(Color.Hex(0x67C23A))
.cornerRadius(5)
.padding(.horizontal, 20)
.padding(.bottom)
}
}
整体款式作用
作用不错!
接下来才是有趣的地方,敲黑板!开始要写逻辑了!
ViewModel部分
取得当时餐段
咱们创立一个新的Swift
文件,命名为ViewModel.swift
。
关于体系取得餐段的思路,咱们能够这么考虑,咱们先取得当时体系的时刻,依据体系时刻所在的时刻段,来更新餐段。示例:
import SwiftUI
class ViewModel: ObservableObject {
// 当时餐段
@Published var currentTimeName: String = ""
init() {
updateTime()
}
// 餐段枚举
enum MealTimeName: String {
case breakfast = "早餐"
case lunch = "午饭"
case afternoonTea = "下午茶"
case supper = "晚餐"
case nightSnack = "宵夜"
}
// 获取当时体系时刻
func getCurrentTime() -> Int {
let dateformatter = DateFormatter()
dateformatter.dateFormat = "HH"
return Int(dateformatter.string(from: Date()))!
}
// 更新当时餐段
func updateTime() {
if getCurrentTime() < 10 {
currentTimeName = MealTimeName.breakfast.rawValue
} else if getCurrentTime() >= 10 && getCurrentTime() < 14 {
currentTimeName = MealTimeName.lunch.rawValue
} else if getCurrentTime() >= 14 && getCurrentTime() < 16 {
currentTimeName = MealTimeName.afternoonTea.rawValue
} else if getCurrentTime() >= 16 && getCurrentTime() < 20 {
currentTimeName = MealTimeName.supper.rawValue
} else {
currentTimeName = MealTimeName.nightSnack.rawValue
}
}
}
上述代码中,咱们先声明晰一个变量currentTimeName
,来作为更新餐段的参数。
然后设置了一个餐段称号的枚举MealTimeName
,来表示餐段和对应餐段的称号。
再是界说了一个办法getCurrentTime
取得当时时刻,只取值到小时,再界说了一个办法updateTime
来依据取得到的时刻和一些时刻段做比较,更新currentTimeName
的值。
最终在init
调用时调用updateTime
更新办法,就得到了当时的餐段currentTimeName
的准确值。
更新当时餐段
咱们回到ContentView
文件中,首要将原先声明的变量DefaultTime
加一个存储办法,示例:
@State var DefaultTime:String = "午饭"
然后引进ViewModel
的内容,示例:
@ObservedObject private var viewModel = ViewModel()
在主视图展现时,更新当时餐段,示例:
.onAppear(){
DefaultTime = viewModel.currentTimeName
}
切换当时餐段
App
除了依据体系时刻主动判别餐段外,咱们还能够增加一个可供用户手艺切换餐段的交互。
咱们能够运用Sheet
弹窗来做切换,首要先创立款式部分,示例:
// 切换餐段
private var ChooseTimeSheet: ActionSheet {
let action = ActionSheet(
title: Text("餐段"),message: Text("请挑选餐段"),buttons:[
.default(Text("早餐"), action: {self.DefaultTime = "早餐"}),
.default(Text("午饭"), action: {self.DefaultTime = "午饭"}),
.default(Text("下午茶"), action: {self.DefaultTime = "下午茶"}),
.default(Text("晚餐"), action: {self.DefaultTime = "晚餐"}),
.default(Text("宵夜"), action: {self.DefaultTime = "宵夜"}),
.cancel(Text("取消"), action: {})
]
)
return action
}
咱们创立了一个Sheet
弹窗,它有几个可选项,当咱们点击不同餐段称号时,更新DefaultTime
餐段的值。
Sheet
弹窗款式创立好后,咱们声明一个变量来供点击触发,示例:
@State var showChooseTimeSheet: Bool = false
然后在ContentView
视图中调用Sheet
弹窗,示例:
// 挑选餐段
.actionSheet(isPresented: $showChooseTimeSheet, content: { ChooseTimeSheet })
至于触发条件,咱们加在点击TitleView
标题视图右边的Image
上,示例:
.onTapGesture {
self.showChooseTimeSheet.toggle()
}
不错不错!
网络恳求数据
让咱们回到ViewModel.swift
文件,咱们来完结网络恳求部分。
@Published var currentTimeName: String = ""
@Published var currentImageURL: String = ""
@Published var currentName: String = ""
@Published var models: [Model] = []
let DataURL = "https://api.npoint.io/4e97acfc3e5f73300779"
首要,咱们要声明好ViewModel
需要的信息,后边在View
中进行赋值,咱们声明晰餐品图片地址currentImageURL
、餐品称号currentName
、存储的数组models
,还有恳求数据的地址DataURL
。
然后是网络恳求部分,示例:
// 网络恳求
func getMenu() {
let session = URLSession(configuration: .default)
session.dataTask(with: URL(string: DataURL)!) { data, _, _ i
guard let jsonData = data else { return }
do {
let meals = try JSONDecoder().decode([Model].self, from: jsonData)
self.models = meals
} catch {
print(error)
}
}
.resume()
}
上述代码中,咱们界说了一个办法getMenu
,经过URLSession
取得数据源地址DataURL
的数据,而且解析到models
中。
这样在调用getMenu
办法时,咱们就能够从DataURL
地址中取得Json
格式的数据,并解析数据按照咱们Model
声明好的参数进行存储。
挑选餐段数据
下一步,由于咱们恳求回来的数据是一切餐段的数据,而咱们每次App引荐的是单个餐段的数据,那么咱们还需要从恳求回来的一切数据傍边挑选出当时挑选的餐段的数据。示例:
// 依据餐段取得餐品信息
func getMealMessage(time:String) {
let query = time.lowercased()
DispatchQueue.global(qos: .background).async {
let filter = self.models.filter { $0.foodTime.lowercased().contains(query) }
DispatchQueue.main.async {
withAnimation(.spring()) {
self.models = filter
}
}
}
}
上述代码中,咱们界说了一个办法getMealMessage
,传入String
类型的餐段时刻time
,然后将time
作为匹配项,与models
数组中的foodTime
进行匹配关联。
找到餐段时刻和数组中的餐段时刻一致的数据,就把相关数据从头存储到models
数组中,这样咱们依据餐段挑选出来了餐品信息。
随机引荐餐品
咱们经过网络恳求getMenu
办法取得了一切餐段的餐品数据,再经过getMealMessage
办法依据餐段挑选出来本餐段的数据,下一步就是在这个餐段的数据中随机引荐餐品,示例;
//随机引荐菜品
func getRandomFood() {
let index = Int(arc4random() % UInt32(models.count))
currentName = models[index].foodName
currentImageURL = models[index].foodImageURL
}
上述代码中,咱们界说了一个办法getRandomFood
,在办法中,咱们从models
数组中的一切数据总量生成一个随机数index
,然后餐品称号currentName
赋值models
数组中随机数index
下标的foodName
,同理餐品图片currentImageURL
也是。
这样咱们就得到了一个取得该餐段随机餐品的办法。
咱们先在viewModel
初始化时,调用取得餐品数据和依据餐段挑选产品的办法。示例:
init() {
updateTime()
getMenu()
}
ViewModel办法调用
咱们回到ContentView.swift
文件,咱们在View
中依据事务调用ViewModel
中的办法。
首要,原先声明的变量都需要运用@State
关键字,以便于实现存储。示例:
@State var DefaultTime: String = "午饭"
@State var DefaultImageURL: String = "https://img0.baidu.com/it/u=156558209,1663147989&fm=253&fmt=auto&app=138&f=JPEG?w=626&h=500"
@State var DefaultName: String = "今日想吃点啥?"
然后在ChooseBtn
按钮上增加交互动作,当咱们点击一键引荐时,查找依据当时餐段挑选数据,然后调用随机餐品的办法,最终将餐品称号和餐品图片赋值到View
中,示例:
// 引荐按钮
func ChooseBtn() -> some View {
Button(action: {
viewModel.getMealMessage(time: DefaultTime)
viewModel.getRandomFood()
DefaultImageURL = viewModel.currentImageURL
DefaultName = viewModel.currentName
}) {
Text("一键引荐")
.font(.system(size: 17))
.fontWeight(.bold)
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.foregroundColor(.white)
.background(Color.Hex(0x67C23A))
.cornerRadius(5)
.padding(.horizontal, 20)
.padding(.bottom)
}
}
当然不要忘了,咱们还有切换餐段的功能呢,在切换餐段时,咱们还需要从头赋值。示例:
self.DefaultTime = "早餐"
viewModel.getMenu()
DefaultImageURL = "https://img0.baidu.com/it/u=156558209,1663147989&fm=253&fmt=auto&app=138&f=JPEG?w=626&h=500"
DefaultName = "今日想吃点啥?"
上述代码中,当咱们切换餐段的时候,除了餐段时刻DefaultTime
从头赋值外,咱们还调用网络恳求从头更新models
数组的数据,以及将餐品称号和餐品图片。
点击按钮预览下作用:
交互动画
动画部分是SwiftUI
的灵魂,承接着用户和App
之间沟通的渠道。
动画部分咱们能够做简单一点,比如在引荐时给个加载动画,引荐成功后展现引荐成果。
Loading动画
咱们创立一个新的SwiftUI
文件,命名为LoadingView
。
import SwiftUI
struct LoadingView: View {
@State var show: Bool = false
var body: some View {
Image(systemName: "sun.min.fill")
.resizable()
.foregroundColor(Color.Hex(0xFAD0C4))
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.rotationEffect(.degrees(show ? 360 : 0))
.onAppear(perform: {
doAnimation()
})
}
func doAnimation() {
withAnimation(Animation.easeInOut(duration: 1).repeatForever(autoreverses: true)) {
show.toggle()
}
}
}
咱们创立了一个Image
,然后让它主动旋转,到达加载中的作用。由于之前咱们就用过这段代码,这里就做太多的解说了。
交互动画运用
咱们回到ContentView.swift
文件,声明一个变量来判别是否展现成果,示例:
@State var showResult: Bool = false
然后依据showResult
的值来展现成果还是加载LoadingView
动画,示例:
if !showResult {
CardView(imageURL: DefaultImageURL, name: DefaultName)
} else {
LoadingView()
}
最终,咱们在ChooseBtn
视图点击一键引荐时,进行展现成果的切换,示例:
self.showResult = true
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) {
self.showResult = false
DefaultImageURL = viewModel.currentImageURL
DefaultName = viewModel.currentName
}
上述代码中,咱们在点击一键引荐时,首要修正showResult
的值,展现Loading
,然后在1秒之后,咱们再修正showResult
的值,并赋值从头展现引荐成果。
项目展现
不错不错!
假如本专栏对你有协助,不妨点赞、评论、关注~
我正在参与技能社区创作者签约方案招募活动,点击链接报名投稿。