解码 Swift 中包含特殊字符的网址
Decode URLs with special characters in Swift
我使用的 API 提供 URL 链接,可以包含特殊字符,如“http://es.dbpedia.org/resource/Análisis_de_datos ”(里面的字母“á”)。
这是一个绝对有效的 URL,但是,如果可解码的 class 包含可选的 URL?
变量,则无法解码。
我可以在我的 class 中将 URL?
更改为 String?
并使用类似 URL(string: urlString.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed)
的计算变量,但也许有更优雅的解决方案。
要在 Playground 中重现:
struct Container: Encodable {
let url: String
}
struct Response: Decodable {
let url: URL?
}
let container = Container(url: "http://es.dbpedia.org/resource/Análisis_de_datos")
let encoder = JSONEncoder()
let encodedData = try encoder.encode(container)
let decoder = JSONDecoder()
let response = try? decoder.decode(Response.self, from: encodedData)
// response == nil, as it can't be decoded.
let url = response?.url
您可以扩展 KeyedDecodingContainer 并实现您自己的 URL 解码方法:
extension KeyedDecodingContainer {
func decode(_ type: URL.Type, forKey key: K) throws -> URL {
let string = try decode(String.self, forKey: key)
guard let url = URL(string: string.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? "")
else {
throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "The stringvalue for the key \(key) couldn't be converted into a URL value: \(string)"))
}
return url
}
// decoding an optional URL
func decodeIfPresent(_ type: URL.Type, forKey key: K) throws -> URL? {
try URL(string: decode(String.self, forKey: key).addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? "")
}
}
struct Container: Encodable {
let url: String
}
struct Response: Decodable {
let url: URL
}
let container = Container(url: "http://es.dbpedia.org/resource/Análisis_de_datos")
do {
let encodedData = try encoder.encode(container)
print(String(data: encodedData, encoding: .utf8))
let decoder = JSONDecoder()
let response = try decoder.decode(Response.self, from: encodedData)
print(response)
} catch {
print(error)
}
这将打印:
Optional("{"url":"http:\/\/es.dbpedia.org\/resource\/Análisis_de_datos"}")
Response(url: http://es.dbpedia.org/resource/An%C3%A1lisis_de_datos)
有多种方法可以解决这个问题,但我认为使用 属性 包装器可能是最优雅的:
@propertyWrapper
struct URLPercentEncoding {
var wrappedValue: URL
}
extension URLPercentEncoding: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let str = try? container.decode(String.self),
let encoded = str.addingPercentEncoding(
withAllowedCharacters: .urlFragmentAllowed),
let url = URL(string: encoded) {
self.wrappedValue = url
} else {
throw DecodingError.dataCorrupted(
.init(codingPath: container.codingPath, debugDescription: "Corrupted url"))
}
}
}
然后你就可以像这样使用它,而这个模型的消费者不必知道任何关于它的信息:
struct Response: Decodable {
@URLPercentEncoding let url: URL
}
我使用的 API 提供 URL 链接,可以包含特殊字符,如“http://es.dbpedia.org/resource/Análisis_de_datos ”(里面的字母“á”)。
这是一个绝对有效的 URL,但是,如果可解码的 class 包含可选的 URL?
变量,则无法解码。
我可以在我的 class 中将 URL?
更改为 String?
并使用类似 URL(string: urlString.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed)
的计算变量,但也许有更优雅的解决方案。
要在 Playground 中重现:
struct Container: Encodable {
let url: String
}
struct Response: Decodable {
let url: URL?
}
let container = Container(url: "http://es.dbpedia.org/resource/Análisis_de_datos")
let encoder = JSONEncoder()
let encodedData = try encoder.encode(container)
let decoder = JSONDecoder()
let response = try? decoder.decode(Response.self, from: encodedData)
// response == nil, as it can't be decoded.
let url = response?.url
您可以扩展 KeyedDecodingContainer 并实现您自己的 URL 解码方法:
extension KeyedDecodingContainer {
func decode(_ type: URL.Type, forKey key: K) throws -> URL {
let string = try decode(String.self, forKey: key)
guard let url = URL(string: string.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? "")
else {
throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "The stringvalue for the key \(key) couldn't be converted into a URL value: \(string)"))
}
return url
}
// decoding an optional URL
func decodeIfPresent(_ type: URL.Type, forKey key: K) throws -> URL? {
try URL(string: decode(String.self, forKey: key).addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? "")
}
}
struct Container: Encodable {
let url: String
}
struct Response: Decodable {
let url: URL
}
let container = Container(url: "http://es.dbpedia.org/resource/Análisis_de_datos")
do {
let encodedData = try encoder.encode(container)
print(String(data: encodedData, encoding: .utf8))
let decoder = JSONDecoder()
let response = try decoder.decode(Response.self, from: encodedData)
print(response)
} catch {
print(error)
}
这将打印:
Optional("{"url":"http:\/\/es.dbpedia.org\/resource\/Análisis_de_datos"}")
Response(url: http://es.dbpedia.org/resource/An%C3%A1lisis_de_datos)
有多种方法可以解决这个问题,但我认为使用 属性 包装器可能是最优雅的:
@propertyWrapper
struct URLPercentEncoding {
var wrappedValue: URL
}
extension URLPercentEncoding: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let str = try? container.decode(String.self),
let encoded = str.addingPercentEncoding(
withAllowedCharacters: .urlFragmentAllowed),
let url = URL(string: encoded) {
self.wrappedValue = url
} else {
throw DecodingError.dataCorrupted(
.init(codingPath: container.codingPath, debugDescription: "Corrupted url"))
}
}
}
然后你就可以像这样使用它,而这个模型的消费者不必知道任何关于它的信息:
struct Response: Decodable {
@URLPercentEncoding let url: URL
}