I have a bunch of Codables in my app that are mostly used for JSON, but sometimes I need to upload images along with them, so I use a multipart request. To do that, I've built up a lot of machinery to handle the boundaries, carriage returns, et cetera but it requires manually adding each key value pair to the list of parts. I feel like the Encodable/Decodable protocols were made to obsolesce exactly that kind of thing.
With a JSONEncoder all I have to do is say encoder.encode(myCodable) and it spits out a Data representation of the JSON for that Codable, respecting the various coding keys and values.
Is there some sort of MultipartEncoder that I could use in a similar way? If not, could you point me to some resources for creating a custom Encoder?
Here's an example of what I currently do. Say we have a Codable called Post:
class Post: Codable {
var userId: Int?
var title: String?
var text: String?
var headerImage: Data?
}
I have built some classes called MultipartComponent (which holds a given key/value pair) and MultipartFormData (which holds an array of MultipartComponents that I use to create an InputStream out of those components to assign to URLRequest.httpBodyStream). So to turn Post into an InputStream I have to make a MultipartComponent from each of it's properties, put those into an array, put that array into a MultipartFormData, and then get an InputStream from that:
extension Post {
func multipartData() -> MultipartFormData {
var parts = [MultipartComponent]()
if let userId = userId {
parts.append(MultipartComponent(data: "\(userId)".data(using: .utf8) ?? Data(), name: "post[user_id]", fileName: nil, contentType:"text"))
}
if let title = title {
parts.append(MultipartComponent(data: title.data(using: .utf8) ?? Data(), name: "post[title]", fileName: nil, contentType:"text"))
}
if let text = text {
parts.append(MultipartComponent(data: text.data(using: .utf8) ?? Data(), name: "post[text]", fileName: nil, contentType:"text"))
}
if let headerImage = headerImage {
parts.append(MultipartComponent(data: headerImage, name: "post[header_image]", fileName: "headerImage.jpg", contentType:"text"))
}
return MultipartFormData(parts: parts, boundary: "--boundary-\(Date().timeIntervalSince1970)-boundary--")
}
Once I've written this extension, I can create a URLRequest for it like so:
var urlRequest = URLRequest(url: url)
urlRequest.httpBodyStream = post.multipartData().makeInputStream()
...
This works... fine. Problem is, as you can imagine, writing that extension gets tedious if there are more than a handful of properties, not to mention the pain of adding or removing properties. What I want is to create a custom encoder (or use an existing one) that will allow me to take any existing Codable and encode it as a multipart form instead of JSON. Is there something that I could use to do something like multipartEncoder.encode(post) to get the data representation or input stream for the Codable?
Related
I wanted to add a description to my Data instances so I know what they're meant to be used for.
Eg: "This data is for (user.name)'s profile picture" or "This data is a encoded User instance" etc.
I was going through the properties available to Data instances and saw there was a description and debugDescription property. I tried to set the values for those properties but it seems like I can't since they are get-only properties. Is there any other way to add a description to a Data instance?
Edit:
Wrapping a Data instance as recommended below is a great solution but if there's a way to achieve the same without using a wrapper please let me know.
You can create a light-weight wrapper like following -
import Foundation
struct DescriptiveData: CustomStringConvertible, CustomDebugStringConvertible {
let data: Data
let customDescription: String?
init(data: Data, customDescription: String? = nil) {
self.data = data
self.customDescription = customDescription
}
var description: String { customDescription ?? data.description }
var debugDescription: String { description }
}
Usage
let data = DescriptiveData(data: Data(), customDescription: "data for profile picture")
print(data)
// data for profile picture
Currently our backend has added a dict on channel object as part of the extra data, it looks something like this:
{
// channel stuff from Stream
"extra_data": {
"custom dict": {
"custom field": "custom value"
}
}
}
However it seems that we cannot access to that dict from the iOS client since the channel.extraData type is a ChannelExtraDataCodable which only has two properties: name and imageURL.
Is there a way to access to this custom stuff from the client side?
Thanks in advance.
You need to define your own structure conforming to ChannelExtraDataCodable and set Channel.extraDataType to it.
Example:
struct MyChannelExtraData: ChannelExtraDataCodable {
var name: String?
var imageURL: URL?
var customDict: [String: String]
}
// Before you initialize the client
Channel.extraDataType = MyChannelExtraData.self
For more information on this, you can check the Stream Chat's documentation about custom extra data on iOS.
Currently I have been working on a task of converting code from objective c to swift. The work was going smooth until I occured with a common resuable code that works in objective c but I haven't getting any idea how should I do that in swift.
The scenario working in objective c is.
I have a common function in my dataManager class
- (void)saveRequest:(id)request forId:(NSNumber *)requestId {
WebRequest *requestData = [[WebRequest alloc] initWithEntity:[NSEntityDescription entityForName:WEB_REQUEST inManagedObjectContext:self.context] insertIntoManagedObjectContext:self.context];
requestData.data = [request toJSON];
requestData.requestId = requestId;
requestData.timestamp = [NSDate date];
[self save];
}
in my project the request classes are already created which contains the toJSON function.
from my controller according to user changes I created the request object and passes the request object to this function and this function calls the toJSON function in the request class and everything works in objective c.
But when I convert this function in swift then it didn't support id as function input variable and if I use Any in place of id then it gives an error that Any don't have any toJSON function.
As this function is common different request objects will come from different controllers.
I don't have any idea how should I go further from hear, If anyone have any idea please help me out
Your class should be like
class WebRequest:NSObject
{
var data :Data?
var requestId: NSNumber?
var timestamp: Date?
init(entity:String , insertIntoManagedObjectContext:NSManagedObjectContext)
{
//your code here
}
}
and your code will be as follows
func saveRequest(request:Request, requestId:NSNumber)
{
let requestData = WebRequest(entity: "entityName", insertIntoManagedObjectContext:self.context)
requestData.data = request.toJSON();
requestData.requestId = requestId;
requestData.timestamp = Date()
}
and Request class in which toJson() present
class Request: NSObject
{
//has some members
func toJSON()->Data
{
return Data()
}
}
There is an existing Swift protocol, Codable (or you can do just Encodable if you want, as Codable is merely Encodable and Decodable), which is designed explicitly for representing an object in JSON (or other formats).
You then use JSONEncoder (rather than JSONSerialization, for example) to encode the object into JSON. See Encoding and Decoding Custom Types:
Consider a Landmark structure that stores the name and founding year of a landmark:
struct Landmark {
var name: String
var foundingYear: Int
}
Adding Codable to the inheritance list for Landmark triggers an automatic conformance that satisfies all of the protocol requirements from Encodable and Decodable:
struct Landmark: Codable {
var name: String
var foundingYear: Int
}
You can then do:
let landmark = Landmark(name: "Big Ben", foundingYear: 1859)
do {
let data = try JSONEncoder().encode(landmark)
print(String(data: data, encoding: .utf8)!)
} catch {
print(error)
}
That will product JSON like so:
{
"name": "Big Ben",
"foundingYear": 1859
}
See that Encoding and Decoding Custom Types for more information.
But, if you make your types Codable/Encodable, you could then retire your toJSON method entirely. There’s no need to write code to encode JSON anymore.
If you’re looking for a more tactical edit to your project as you convert it from Objective-C to Swift, you could define your own protocol, say JsonRepresentable, that has a single method requirement, your toJSON (or to whatever you’ve renamed this method during your conversion process).
protocol JsonRepresentable {
func toJSON() -> Data
}
And then, for all of the types that have implemented this method, just add this conformance.
Ideally, go back to those individual files and move the method into an extension for that protocol, e.g., for your first object type:
extension RequestObject1: JsonRepresentable {
func toJSON() -> Data {
...
}
}
And for your second:
extension RequestObject2: JsonRepresentable {
func toJSON() -> Data {
...
}
}
Etc.
is not there a simpler way rather than changing it in whole project
I would suggest that the above is best, but, if you don’t want to go back to all of those individual type declarations, you can just add conformance with an empty extension right where you defined JsonRepresentable:
extension RequestObject1: JsonRepresentable { }
extension RequestObject2: JsonRepresentable { }
As long as those types have implemented that method, these extensions will let the compiler know about their conformance to your protocol.
Anyway, this method can then use this protocol:
func save(_ request: JsonRepresentable, requestId: Int) {
let requestData = ...
requestData.data = request.toJSON()
requestData.requestId = requestId
requestData.timestamp = Date()
save()
}
I am using Objectmapper and Realm for my project.
I have an object like following
class File
{
dynamic var name
dynamic var folder
dynamic var path // This is not coming from JSON // this should be combination of both name+folder
}
I thought of writing a computed property to achieve this but Realm does not support computed properties as primary key.
But I should use this as primary key. Is there any way I can manipulate to add that value after coming from server response.
Note: I am using AlamofireObjectMapper.
I am using the following method which parses the server response and gives me the model object.
Alamofire.request(router).responseObject{ (response: DataResponse<T>) in
{
let myModel = response.result.value // Parsed object
===== What can i do here to achieve my requirement=====
}
You should really consider having some kind of id as the primary key and not computing it from other properties (what happens if they are empty or the computation goes wrong? You'd be left without a valid primary key).
However, if you really need to, you could try
let realm = try Realm()
try realm.write {
items.forEach({ (item) in
item.path = item.name + item.folder
}
realm.add(items, update: true)
}
and don't forget to define path as the primary key in the File class:
class File
{
dynamic var name
dynamic var folder
dynamic var path // This is not coming from JSON // this should be combination of both name+folder
override static func primaryKey() -> String? {
return "path"
}
}
I'm using a library called Gloss to help parse JSON data. As a result I've created structs that are of type Glossy:
struct LoyaltyCard: Glossy {
let id: Int
init?(json: JSON) {
guard let __id: Int = "id" <~~ json
else { return nil }
}
I have many different Glossy structs and want to pass them into a function along with a string but I keep getting an error: " Cannot invoke 'getMemberInfo' with an argument list of type '(String, memberData: LoyaltyCard.Type)'", here is an abbreviated version of my function:
func getMemberInfo<T: Glossy> (memberDataRequest: String, memberData:T) {
let urlAccess = "\(baseURL)/api/\(memberDataRequest)"
///////code////////////
let data = object as! NSData
let jsonInfo: AnyObject? = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.init(rawValue: 0))
let jsonArray = jsonInfo as! NSArray
if let dict = jsonArray[0] as? JSON //This means as type Gloss
{
let loyaltyID= LoyaltyCard(json: dict)
print(loyaltyID?.id)
}
}
Any ideas how to make this function work?
I'm inferring from your code sample and from your comments, that you don't necessarily want to pass a Glossy type to getMemberInfo, but rather that the key requirement is that you want to perform a network request and return a Glossy type.
While I get what you were trying to do, I would personally retire the generic approach, and just use a protocol extension. You end up with a method that can be called for any Glossy type. And if this protocol extension method returns a type Self, that will end up returning whatever Glossy type from which you happen to call it.
First, let's step back and be clear as to what the Glossy protocol might look like. At the very least, you'd have some failable initializer (plus whatever else your types needed):
protocol Glossy {
init?(json: [String: AnyObject])
}
(Note, I'm not using JSON type, but feel free if you want. I personally just use Swift collections for parsed JSON, but do whatever you want.)
I'd then define a static method in a protocol extension to perform the request. The following method uses NSURLSession, but if you use Alamofire or something else, the basic idea is the same:
extension Glossy {
static func performMemberRequest(memberDataRequest: String, completionHandler:(Self?, ErrorType?) -> ()) -> NSURLSessionTask {
let urlAccess = "\(baseURL)/api/\(memberDataRequest)"
let request = NSMutableURLRequest(URL: NSURL(string: urlAccess)!)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
guard let data = data where error == nil else {
completionHandler(nil, error)
return
}
do {
if let array = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [[String: AnyObject]],
let dictionary = array.first {
completionHandler(Self(json: dictionary), nil)
} else {
completionHandler(nil, GlossyError.InvalidJSONError)
}
} catch let parseError {
completionHandler(nil, parseError)
}
}
task.resume()
return task
}
}
Note, there are a few notable issues entailed in the above:
Network requests should always be performed asynchronously. So use asynchronous pattern like completionHandler rather than trying to return some object immediately.
If you're going to return anything, the only thing you should be returning is the NSURLSessionTask object so the caller has the option to capture that and cancel requests, should you want that functionality.
I changed the name of the method to be more descriptive and conform to Cocoa naming conventions.
As an aside, your code suggests that your API returned an array and you're just grabbing the first dictionary. That seems like a curious pattern, but I've followed that, above. If you really were returning an array, it strikes me that you did that because you contemplate a scenario where you could be returning multiple items. In that case, I would advise iterate through the whole array and have the completion handler return [Self]? (an array of Glossy objects) rather than just Self? (i.e. only the first one).
Furthermore, I wouldn't personally advise a structure that returns an array as the top level item. How does that web service report errors? I'd have a dictionary structure that returned success/failure and/or return code and the like. And then have a dedicated key for results which would be your array of results.
But I didn't tackle any of these broader API issues above, but rather followed the pattern in your code snippet. But these are considerations you might want to think about.
In my example, I didn't dispatch these completionHandler calls back to the main queue, but that's often a very useful pattern (avoids synchronization problems, UI updates, etc.). It's trivial to do, but I wanted to keep the above relatively simple.
But, let's step aside from the details of your API and the like. Let's focus on the notion that you want a static method defined in the protocol extension, (and it can therefore be called from any type that conforms to Glossy). For example, I can then define the LoyaltyCard class with the required initializer:
struct LoyaltyCard: Glossy {
let id: Int
init?(json: [String: AnyObject]) {
guard let id = json["id"] as? Int else {
return nil
}
self.id = id
}
}
Having done all that, I can now invoke the static method of Glossy protocol extension on LoyaltyCard, for example:
LoyaltyCard.performMemberRequest(memberDataRequest) { loyaltyCard, error in
guard let loyaltyCard = loyaltyCard where error == nil else {
print(error)
return
}
// do something with loyaltyCard here
print(loyaltyCard)
}
// but don't use it here
There's a lot there, but I don't want you to get lost in the details. But I do hope you grok the key concepts here: Don't pass a Glossy type to your method, nor use a generic: Instead use protocol extension. And avoid synchronous network requests, so instead use a asynchronous pattern like the completionHandler pattern.