项目档案

为类、dto和网关建模

由于这个赢博体育程序将使用SwiftData和一个与服务器通信的网关,我们需要创建一些类来表示我们将存储在数据库中的数据和我们将从服务器来回传递的对象。不幸的是,SwiftData模型类不能很好地与JSON编码器/解码器一起工作,所以我们必须为每个实体创建两个类:

@模型最终类物种{@属性(.unique) var id: Int var名称:字符串var图像:字符串init(dto: SpeciesDTO){自我。Id = dto。Idspecies self.name = dto.name self。Image = dto。final class SpeciesDTO: Codable {var idspecies: Int var name: String var image: String} @Model final class Park {@Attribute(.unique) var id: Int var name: String var county: String init(dto: ParkDTO) {self。Id = dto。Idpark self.name = dto.name self。县= dto。final class ParkDTO: Codable {var idpark: Int var name: String var county: String} @Model final class Sighting {@Attribute(.unique) var id: String = UUID()。uuidString var user: Int var species: Int var park: Int var comment: String var date: String init(user: Int, species: Int, park: Int, comment: String, date: String){自我。User =用户自身。物种=物种自身。Park = Park self.comment = comment self。日期=日期}}最终类SightingDTO:可编码{var用户:Int var物种:Int var公园:Int var评论:字符串var日期:字符串init(sighting: sighting){自我。User = sighting。用户自我。物种=目击。物种的自我。公园=观光。Park self.comment = seeing .comment self。约会=约会。最后一个类UserDTO: Codable {var iduser: Int = 0 var username: String var password: String var realname: String var phone: String init(username: String, password: String, realname: String, phone: String) {self. date}}Username =用户名self。Password = Password self。Realname = Realname self。电话=电话}}

从服务器来回传递的类有时被称为“数据传输对象”,简称dto。

另一个隐晦的问题,我不得不应付这里是SwiftData已经包含了一个类名为“观察”。这迫使我将观察对象重命名为Sighting对象,以防止与SwiftData观察类的名称冲突。

网关

关于这个解决方案的Gateway类的一个有趣的事实是,它专门处理服务器通信,而不处理SwiftData。正如我们将在下面看到的,赢博体育的SwiftData代码都将出现在视图类中。这样做的主要原因是视图类将自动提供模型上下文作为环境变量,而网关则不会。这很重要,因为@Query变量只有在模型上下文可用时才能正常工作。

下面是Gateway类的代码:

类网关:ObservableObject{@发布var userid: Int?func fetchSpecies() async -> [SpeciesDTO]{让urlStr = "https://cmsc106.net/invasive/species"守卫让url = url(字符串:urlStr) else{返回[]}do{让(数据,响应)=尝试等待URLSession.shared。数据(来自:url)守卫(响应为!)HTTPURLResponse)。statusCode == 200 else {return[]}让结果= try JSONDecoder().decode([SpeciesDTO].self, from: data)返回结果}catch {print("Error in fetchSpecies: \(Error)")} return []} func fetchParks() async -> [ParkDTO] {let urlStr = "https://cmsc106.net/invasive/parks" guard让url = url (string: urlStr) else {return []} do {let (data, response) = try await URLSession.shared。数据(来自:url)守卫(响应为!)HTTPURLResponse)。statusCode == 200 else {return[]}让结果= try JSONDecoder().decode([ParkDTO].self, from: data)返回结果}catch {print(" fetchParks: \(Error)")} return []} func newUser(_ dto: UserDTO) async -> Bool{让urlStr = "https://cmsc106.net/invasive/users" guard让url = url (string: urlStr) else{返回false} var request = URLRequest(url: url)请求。httpMethod = "POST"请求。setValue("application/json", forHTTPHeaderField: "Content-Type") do {let jsonData = try JSONEncoder().encode(dto) let (data, response) = try await URLSession.shared。上传(for: request, from: jsonData) guard(响应为!)HTTPURLResponse)。statusCode == 200 else{返回false} let result = try JSONDecoder().decode(UserDTO.self, from: data) DispatchQueue.main.async {self. data}Userid = result。iduser}返回true} catch {print("Error in newUser: \(Error)")}返回false} func login(user: String,password: String) async -> Bool {let urlStr = "https://cmsc106.net/invasive/users?user=\(user)&password=\(password)" guard let url = url (String: urlStr) else{返回false} do {let (data, response) = try await URLSession.shared。数据(来自:url)守卫(响应为!)HTTPURLResponse)。statusCode == 200 else{返回false} let result = try JSONDecoder().decode(UserDTO.self, from: data) DispatchQueue.main.async {self. data}Userid = result。iduser}返回true} catch {print("Error in login: \(Error)")}返回false} func uploadSighting(_ s: Sighting) async -> Bool {if let id = userid {let urlStr = "https://cmsc106.net/invasive/observations" guard let url = url (string: urlStr) else{返回false} var request = URLRequest(url: url) request。httpMethod = "POST"请求。setValue("application/json", forHTTPHeaderField: "Content-Type") do {let dto = SightingDTO(sighingdto: s) dto。user = id let jsonData = try JSONEncoder().encode(dto) let (_, response) = try await URLSession.shared。上传(for: request, from: jsonData) guard(响应为!)HTTPURLResponse)。statusCode == 200 else{返回false}返回true} catch {print("Error in uploadSighting: \(Error)")}}返回false}}

这里的赢博体育方法都处理我们需要的服务器交互。特别是,这里绝对没有逻辑来决定何时运行这些服务器交互:赢博体育的逻辑都将出现在视图类中。

还要注意,这个网关类只与dto一起工作,对模型类没有任何作用。

着陆视图

当赢博体育程序启动时,用户将看到的第一个视图是着陆视图,这让他们可以访问导航选项和按钮,用于登录和上传他们的观察结果。

“上传观察”按钮和“登录”按钮都是智能的:这些按钮只有在赢博体育时才启用。例如,如果用户尚未登录,则“登录”按钮将被启用。一旦他们登录,按钮将被禁用。同样,“上传观察”按钮只有在用户有一些观察要上传并且他们已经成功登录时才会启用。

下面是这个着陆视图的代码:

struct ContentView: View {@Environment(\.modelContext) private var modelContext @StateObject var gateway: gateway @Query var sightings: [Sighting] @State var showSigninSheet: Bool = false var body: some View {NavigationStack {VStack {NavigationLink("Gallery") {GalleryView(gateway: gateway)} .padding() .background(.green) .foregroundColor(.white) .cornerRadius(10) NavigationLink("Record Observation") {RecordingView(gateway: gateway):} .padding() .background(.green) .foregroundColor(.white) .cornerRadius(10) Button("Upload Observations") {for s in sightings {Task {if await gateway. uploadsighting (s) {DispatchQueue.main.async {modelContext.delete(s) try?modelContext.save()}}}}} .disabled(网关。userid == nil || sightings.isEmpty) .padding() Button(" login ") {showSigninSheet = true} .disabled(gateway.;)userid != nil) .padding() .sheet(ispresent: $showSigninSheet, content: {LoginView(gateway: gateway)})}}}}

我们使用.disabled()修饰符控制底部两个按钮的启用/禁用状态。这个修饰符的参数是一个布尔表达式,该表达式决定是否禁用按钮。

画廊视图

为了满足问题陈述中的要求,赢博体育程序应该包括一个入侵物种图库和植物图片,我建立了一个简单的图库视图。

下面是该视图的代码:

struct GalleryView: View {@Environment(\.modelContext) private var modelContext @StateObject var gateway: gateway @Query var species: [species] var body: some View {List {ForEach(species) {s in HStack {AsyncImage(url: url (string: .image)) {image in image .resizable() . aspectratio (contentMode: .fill)}占位符:{Color。gray}. frame(width: 150, height: 100) Text(s.name)}}}. onappear(){如果物种。isEmpty {Task {let dtos = await gateway.fetchSpecies() DispatchQueue.main.async {for dtos中的dto {let s = Species(dto: dto) modelContext.insert(s)} try?modelContext.save()}}}}}}

在这里,我们将第一次看到一些SwiftData代码。注意这里的@Query从数据库中获取一个物种对象数组。我们需要注意的一件事是,当用户第一次启动赢博体育程序时,我们还没有从服务器获取那些对象。我在onAppear()修饰符中添加了一些逻辑来检查这种情况。这里的逻辑将要求网关在必要时从服务器获取我们的speciesdto。一旦我们有了这些,我们就可以将它们存储在数据库中。下次用户转到此视图时,该查询列表将不是空的。

注意,这里的List组件将显示物种列表中的那些物种。

记录观察结果

赢博体育程序中的最终视图允许用户记录他们的观察结果。

下面是这个视图的代码:

struct RecordingView: View {@Environment(\.modelContext) private var modelContext @StateObject var gateway: gateway @Query var parks: [Park] @State var selectedPark: Park?@查询var物种:[物种]@状态var selectedSpecies:物种?@State var comment: String = "" @State var date: String = "" var body: some View {VStack {Picker("Park“,选择:$selectedPark) {ForEach(parks) {Park in Text(Park .name).tag(Park .self as Park?)}} Picker(”Species",选择:$selectedSpecies) {ForEach(species) {species in Text(species.name).tag(species.self as species ?)}} TextField(“输入评论”,文本:$comment) TextField(“输入日期”,文本:$date) Button(“保存观察”){if let sp = selectedPark {if let ss = selectedSpecies {let obs = Sighting(用户:网关)。userid ? ?0,物种:ss.id,公园:sp.id,评论:评论,日期:日期)modelContext.insert(obs) try?modelContext.save()}}}}.padding() . onappear(){如果物种。isEmpty {Task {let sdtos = await gateway.fetchSpecies() DispatchQueue.main.async {for dto in sdtos {let s = Species(dto: dto) modelContext.insert(s)} try?modelContext.save()}}}isEmpty {Task {let pdtos = await gateway.fetchParks() DispatchQueue.main.async {for dto in pdtos {let p = Park(dto: dto) modelContext.insert(p)} try?modelContext.save()}}}}}}

与图库视图一样,这个视图必须进行检查,以确保它拥有完成工作所需的对象。特别是,由于我们需要在这里显示公园列表和物种列表,因此我添加了代码,以便在需要时从服务器下载这些列表。

有一件事我在这里没有做的是添加逻辑,使找到一个特定的公园更容易。我把它留给读者作为练习。

当用户在这里保存观察结果时,唯一会发生的事情就是我们将在数据库中保存一个Sighting对象。回想一下,登陆页面有工具允许用户在准备好时将收集到的观察结果上传到服务器。