我正在尝试创建一个Codable extension,它只能用一个json字符串初始化一个Decodable(Swift 4)对象。那应该是什么工作:
struct MyObject: Decodable {
var title: String?
}
let myObject = MyObject(json: "{\"title\":\"The title\"}")
我认为这意味着我应该创建一个使用Decoder调用self.init的init。以下是我提出的代码:
public init?(json: String) throws {
guard let decoder: Decoder = GetDecoder.decode(json: json)?.decoder else { return }
try self.init(from: decoder) // Who what should we do to get it so that we can call this?
}
该代码能够获取解码器,但是在调用init时出现编译器错误。我得到的错误是:
'自'在self.init call之前使用
这是否意味着无法在Decodable协议中添加init?
有关完整的源代码,请参阅codable extension on github
更新
从@appzYourLive调试下面的解决方案后,我发现我与Decodable和Array上的init(json:
初始化程序有冲突。我刚刚向GitHub发布了新版本的扩展。我还添加了解决方案作为这个问题的新答案。
答案 0 :(得分:6)
可能的解决方法
可能的解决方案是定义另一个协议
protocol DecodableFromString: Decodable { }
拥有自己的初始化程序
extension DecodableFromString {
init?(from json: String) throws {
guard let data = try json.data(using: .utf8) else { return nil }
guard let value = try? JSONDecoder().decode(Self.self, from: data) else { return nil }
self = value
}
}
现在您需要将您的类型符合DecodableFromString
struct Person:Codable, DecodableFromString {
let firstName: String
let lastName: String
}
最后给了一个JSON
let json = """
{
"firstName": "Luke",
"lastName": "Skywalker"
}
"""
你可以建立你的价值
if let luke = try? Person(from: json) {
print(luke)
}
人(firstName:" Luke",lastName:" Skywalker")
答案 1 :(得分:2)
这是可以做到的:
extension Decodable {
init?(jsonString: String) throws {
guard let jsonData = jsonString.data(using: .utf8) else { return nil }
self = try JSONDecoder().decode(Self.self, from: jsonData)
}
}
请参阅:https://bugs.swift.org/browse/SR-5356
[UPD] XCode 9 beta 3修复了问题(详见上述链接)。
答案 2 :(得分:-1)
我原来的问题是由以下两个原因造成的:
因此,由于使用可用的初始化程序存在问题,我最终得到了:
public extension Decodable {
init(json: String) throws {
guard let data = json.data(using: .utf8) else { throw CodingError.RuntimeError("cannot create data from string") }
try self.init(data: data, keyPath: keyPath)
}
init(data: Data) throws {
self = try JSONDecoder().decode(Self.self, from: data)
}
}
enum CodingError : Error {
case RuntimeError(String)
}
我还做了一个变体,你可以使用keyPath跳转到某个部分:
public extension Decodable {
init(json: String, keyPath: String? = nil) throws {
guard let data = json.data(using: .utf8) else { throw CodingError.RuntimeError("cannot create data from string") }
try self.init(data: data, keyPath: keyPath)
}
init(data: Data, keyPath: String? = nil) throws {
if let keyPath = keyPath {
let topLevel = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers)
guard let nestedJson = (topLevel as AnyObject).value(forKeyPath: keyPath) else { throw CodingError.RuntimeError("Cannot decode data to object") }
let nestedData = try JSONSerialization.data(withJSONObject: nestedJson)
self = try JSONDecoder().decode(Self.self, from: nestedData)
return
}
self = try JSONDecoder().decode(Self.self, from: data)
}
}
您可以在下面找到我的可解码和可编码扩展程序的完整代码。它也在我的GitHub子规格中。使用此扩展程序,您可以使用以下代码:
struct YourCodableObject : Codable {
var naam: String?
var id: Int?
}
let json = yourEncodableObjectInstance.toJsonString()
let data = yourEncodableObjectInstance.toJsonData()
let newObject = try? YourCodableObject(json: json)
let newObject2 = try? YourCodableObject(data: data)
let objectArray = try? [YourCodableObject](json: json)
let objectArray2 = try? [YourCodableObject](data: data)
let newJson = objectArray.toJsonString()
let innerObject = try? TestCodable(json: "{\"user\":{\"id\":1,\"naam\":\"Edwin\"}}", keyPath: "user")
try initialObject.saveToDocuments("myFile.dat")
let readObject = try? TestCodable(fileNameInDocuments: "myFile.dat")
try objectArray.saveToDocuments("myFile2.dat")
let objectArray3 = try? [TestCodable](fileNameInDocuments: "myFile2.dat")
以下是2个扩展名:
//
// Codable.swift
// Stuff
//
// Created by Edwin Vermeer on 28/06/2017.
// Copyright © 2017 EVICT BV. All rights reserved.
//
enum CodingError : Error {
case RuntimeError(String)
}
public extension Encodable {
/**
Convert this object to json data
- parameter outputFormatting: The formatting of the output JSON data (compact or pritty printed)
- parameter dateEncodinStrategy: how do you want to format the date
- parameter dataEncodingStrategy: what kind of encoding. base64 is the default
- returns: The json data
*/
public func toJsonData(outputFormatting: JSONEncoder.OutputFormatting = .compact, dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .deferredToDate, dataEncodingStrategy: JSONEncoder.DataEncodingStrategy = .base64Encode) -> Data? {
let encoder = JSONEncoder()
encoder.outputFormatting = outputFormatting
encoder.dateEncodingStrategy = dateEncodingStrategy
encoder.dataEncodingStrategy = dataEncodingStrategy
return try? encoder.encode(self)
}
/**
Convert this object to a json string
- parameter outputFormatting: The formatting of the output JSON data (compact or pritty printed)
- parameter dateEncodinStrategy: how do you want to format the date
- parameter dataEncodingStrategy: what kind of encoding. base64 is the default
- returns: The json string
*/
public func toJsonString(outputFormatting: JSONEncoder.OutputFormatting = .compact, dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .deferredToDate, dataEncodingStrategy: JSONEncoder.DataEncodingStrategy = .base64Encode) -> String? {
let data = self.toJsonData(outputFormatting: outputFormatting, dateEncodingStrategy: dateEncodingStrategy, dataEncodingStrategy: dataEncodingStrategy)
return data == nil ? nil : String(data: data!, encoding: .utf8)
}
/**
Save this object to a file in the temp directory
- parameter fileName: The filename
- returns: Nothing
*/
public func saveTo(_ fileURL: URL) throws {
guard let data = self.toJsonData() else { throw CodingError.RuntimeError("cannot create data from object")}
try data.write(to: fileURL, options: .atomic)
}
/**
Save this object to a file in the temp directory
- parameter fileName: The filename
- returns: Nothing
*/
public func saveToTemp(_ fileName: String) throws {
let fileURL = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(fileName)
try self.saveTo(fileURL)
}
#if os(tvOS)
// Save to documents folder is not supported on tvOS
#else
/**
Save this object to a file in the documents directory
- parameter fileName: The filename
- returns: true if successfull
*/
public func saveToDocuments(_ fileName: String) throws {
let fileURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(fileName)
try self.saveTo(fileURL)
}
#endif
}
public extension Decodable {
/**
Create an instance of this type from a json string
- parameter json: The json string
- parameter keyPath: for if you want something else than the root object
*/
init(json: String, keyPath: String? = nil) throws {
guard let data = json.data(using: .utf8) else { throw CodingError.RuntimeError("cannot create data from string") }
try self.init(data: data, keyPath: keyPath)
}
/**
Create an instance of this type from a json string
- parameter data: The json data
- parameter keyPath: for if you want something else than the root object
*/
init(data: Data, keyPath: String? = nil) throws {
if let keyPath = keyPath {
let topLevel = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers)
guard let nestedJson = (topLevel as AnyObject).value(forKeyPath: keyPath) else { throw CodingError.RuntimeError("Cannot decode data to object") }
let nestedData = try JSONSerialization.data(withJSONObject: nestedJson)
let value = try JSONDecoder().decode(Self.self, from: nestedData)
self = value
return
}
self = try JSONDecoder().decode(Self.self, from: data)
}
/**
Initialize this object from an archived file from an URL
- parameter fileNameInTemp: The filename
*/
public init(fileURL: URL) throws {
let data = try Data(contentsOf: fileURL)
try self.init(data: data)
}
/**
Initialize this object from an archived file from the temp directory
- parameter fileNameInTemp: The filename
*/
public init(fileNameInTemp: String) throws {
let fileURL = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(fileNameInTemp)
try self.init(fileURL: fileURL)
}
/**
Initialize this object from an archived file from the documents directory
- parameter fileNameInDocuments: The filename
*/
public init(fileNameInDocuments: String) throws {
let fileURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(fileNameInDocuments)
try self.init(fileURL: fileURL)
}
}