我的iOS应用程序有一个非常常见的设置:它向使用JSON对象响应的API服务器进行HTTP查询。然后将这些JSON对象解析为适当的Swift对象。
最初,我将属性划分为必需的属性和可选属性,主要基于我的API服务器的数据库要求。例如,id
,email
和name
是必填字段,因此它们使用非可选类型。其他人可以在数据库中NULL
,因此它们是可选类型。
class User {
let id: Int
let email: String
let profile: String?
let name: String
let motive: String?
let address: String?
let profilePhotoUrl: String?
}
最近,我开始怀疑这是否是一个很好的设置。我发现尽管某些属性可能总是在数据库中,但这并不意味着这些属性将始终包含在JSON响应中。
例如,在“用户个人资料”页面中,需要所有这些字段才能正确显示视图。因此,JSON响应将包括所有这些字段。但是,对于列出用户名称的视图,我不需要email
或id
,JSON响应也可能不包含这些属性。不幸的是,这会在将JSON响应解析为Swift对象时导致错误并使应用程序崩溃,因为应用程序希望id
,email
,name
始终不为零。
我正在考虑将Swift对象的所有属性更改为可选对象,但这样可能会丢掉这种特定于语言的功能的所有好处。此外,我还需要编写更多代码行,以便在应用程序的其他位置解包所有这些选项。
另一方面,JSON对象本质上不能与Swift的严格静态类型和nil检查非常互操作,所以最好简单地接受这种烦恼。
我应该转换为每个属性作为选项的模型吗?或者,还有更好的方法?我很感激这里有任何评论。
答案 0 :(得分:14)
有三种方法可以解决这个问题:
始终发送所有JSON数据,并使您的属性不可选。
使所有属性都可选。
将所有属性设置为非可选属性,并编写您自己的init(from:)
方法,为缺省值指定默认值,如this answer中所述。
所有这些都应该有效;哪一个是最好的"是基于意见的,因此超出了Stack Overflow答案的范围。选择最适合您特定需求的那个。
答案 1 :(得分:9)
首先要问的是:“列出用户名称的视图”的元素是否需要与“用户个人资料页面”后面的模型对象属于同一类型的对象?也许不是。也许您应该专门为用户列表创建一个模型:
struct UserList: Decodable {
struct Item: Decodable {
var id: Int
var name: String
}
var items: [Item]
}
(虽然问题说JSON响应可能不包含id
,但它似乎不是没有id的用户列表特别有用,所以我在这里要求它。)
如果你真的希望它们是同一类型的对象,那么你可能希望将用户建模为具有服务器始终发送的核心属性,以及可能为零的“详细信息”字段:
class User: Decodable {
let id: Int
let name: String
let details: Details?
struct Details: Decodable {
var email: String
var profile: String?
var motive: String?
var address: String?
var profilePhotoUrl: String?
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
details = container.contains(.email) ? try Details(from: decoder) : nil
}
enum CodingKeys: String, CodingKey {
case id
case name
case email // Used to detect presence of Details
}
}
请注意,我使用Details
而不是通常的Details(from: decoder)
创建container.decode(Details.self, forKey: .details)
(如果存在)。我使用Details(from: decoder)
来执行此操作,以便Details
的属性来自与User
的属性相同的JSON对象,而不需要嵌套对象。
答案 2 :(得分:4)
前提:
部分表示是REST中的常见模式。这是否意味着所有 Swift中的属性需要选项吗?例如,客户端 可能只需要一个视图的用户ID列表。这是否意味着所有 其他属性(名称,电子邮件等)需要标记为可选吗? 这是斯威夫特的好习惯吗?
在模型中标记属性可选仅表示密钥可能会或可能不会。它允许读者在第一眼看上去了解模型的某些事情
如果您只为不同的API响应结构维护一个通用模型并使所有属性都是可选的,那么这是否是一个好的做法是非常有争议的。
我已经这样做了它咬了。有时它很好,有时候还不够清楚。
为多个API保留一个模型就像设计一个具有许多UI元素的ViewController
并根据具体情况确定应显示的UI元素。
这增加了新开发人员的学习曲线,因为对系统的了解更多。
假设我们正在使用Swift的Codable
进行编码/解码模型,我会将其分解为单独的模型,而不是维护具有所有选项和/或默认值的通用模型。
我决定的理由是:
分离的清晰度
考虑以后可能会出现的API特定附加密钥。
然而,如果我很懒,而且我有强烈的感觉,未来会出现疯狂的变化,我会继续制作所有关键选项并承担相关费用。
答案 3 :(得分:4)
这实际上取决于您处理数据的方式。如果您通过" Codable"处理您的数据。类,然后你必须编写一个自定义解码器,以便在你没有获得某些预期值时抛出异常。像这样:
class User: Codable {
let id: Int
let email: String
let profile: String?
let name: String
let motive: String?
let address: String?
let profilePhotoUrl: String?
//other methods (such as init, encoder, and decoder) need to be added below.
}
因为我知道如果我没有获得所需的最低参数,我将需要返回错误,您需要类似错误枚举的内容:
enum UserCodableError: Error {
case missingNeededParameters
//and so on with more cases
}
您应该使用编码密钥来保持服务器的一致性。在User Object中执行此操作的方法如下:
fileprivate enum CodingKeys: String, CodingKey {
case id = "YOUR JSON SERVER KEYS GO HERE"
case email
case profile
case name
case motive
case address
case profilePhotoUrl
}
然后,你需要编写你的解码器。一种方法就是这样:
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
guard let id = try? values.decode(Int.self, forKey: .id), let email = try? values.decode(String.self, forKey: .email), let name = try? values.decode(String.self, forKey: .name) else {
throw UserCodableError.missingNeededParameters
}
self.id = id
self.email = email
self.name = name
//now simply try to decode the optionals
self.profile = try? values.decode(String.self, forKey: .profile)
self.motive = try? values.decode(String.self, forKey: .motive)
self.address = try? values.decode(String.self, forKey: .address)
self.profilePhotoUrl = try? values.decode(String.self, forKey: .profilePhotoUrl)
}
注意事项:您应该编写自己的编码器以保持一致。
所有这一切都可以,只需要一个很好的调用声明:
if let user = try? JSONDecoder().decode(User.self, from: jsonData) {
//do stuff with user
}
这可能是处理此类问题的最安全,最迅速,最面向对象的方式。
答案 4 :(得分:3)
如果服务器为其他属性提供Null值,您可以选择选项并安全解包。或者在解包时,如果值为nil
,则可以将空字符串赋值给propertyprofile = jsonValue ?? ""
其他情况,因为其他属性是String数据类型,您可以将默认值指定为空字符串
class User {
let id: Int
let email: String
let profile: String = ""
let name: String
let motive: String = ""
let address: String = ""
let profilePhotoUrl: String = ""
}
答案 5 :(得分:3)
我通常会将所有非关键属性设置为可选,然后使用可用的初始化程序。这使我能够更好地处理JSON格式的任何更改或破坏的API契约。
例如:
class User {
let id: Int
let email: String
var profile: String?
var name: String?
var motive: String?
var address: String?
var profilePhotoUrl: String?
}
这意味着我永远不会有没有id或电子邮件的用户对象(让我们假设那些总是需要与用户关联的用户对象)。如果我获得没有id或电子邮件的JSON有效负载,则User类中的Initializer将失败并且不会创建用户对象。然后,我对失败的初始化程序进行了错误处理。
我更倾向于拥有一个带有可选属性的swift类,而不是一堆带有空字符串值的属性。
答案 6 :(得分:3)
是的,如果API中不需要该属性,并且如果您想在强制属性中使用某个值,则应使用可选项,然后指定空白值:
class User {
let id: Int?
let email: String? = ""
let profile: String?
let name: String? = ""
let motive: String?
let address: String?
let profilePhotoUrl: String?
}
答案 7 :(得分:3)
我建议将所有non-scalar(String, Custom Types etc) properties
保留为可选,scalar(Int, Float, Double etc)
作为非可选(有一些例外),方法是指定默认值和带有空数组的集合。 e.g,
class User {
var id: Int = 0
var name: String?
var friends: [User] = []
var settings: UserSettings?
}
无论服务器发生什么情况,这都可以确保您获得无崩溃的应用程序。我宁愿在崩溃时出现异常行为。
答案 8 :(得分:3)
在我看来,我会选择2种解决方案中的一种:
init func
从JSON
编辑为object
,使用所有道具的默认对象值(id = -1, email = ''
)初始化,然后使用可选的检查功能读取JSON。class/struct
。答案 9 :(得分:0)
与其他选项相比,我更倾向于使用可用的初始化程序。
因此,将所需属性保留为非选项并仅在响应中存在对象时创建对象(可以使用if-let或gaurd-let在响应中检查此对象),否则无法创建对象。 / p>
使用这种方法,我们避免将非选项作为选项,并且难以在整个程序中处理它们。
此外,选项不适用于防御性编程,因此不要通过制作非选择性"来滥用选项。属性作为选项。
答案 10 :(得分:-1)
我更喜欢可选属性,因为您无法承诺JSON值始终存在,并且响应属性名称的任何更改都会导致应用程序崩溃。
如果您不使用可选值,则必须在解析时控制参数,并在需要无崩溃应用时添加默认值。你不会知道它是否是来自服务器的空字符串。
可选值是您最好的朋友。
object mapper用于可变和不可变的属性。
realm-swift表示默认的非可选值。