How to edit a structure's properties when its instance is created from a JSON file?

I have a data.json file inside the bundle of a SwiftUI project that contains objects like this:

object
  {
    "id": 7,
    "year": 2017,
    "month": 2,
    "day": 19,
    "date": "19.2.2017",
    "recipient": "Gynko",
    "description": "dolore esse ipsum fugiat esse",
    "amount": 480.78,
    "category": "education",
    "timeStamp": 1
  },

The file gets parsed with JSONDecoder() and the objects are loaded into a variable anArray:[Custom]. So far, all is fine. Then, I would like to overwrite the timeStamp property of each object with this function:

convert date string to time stamp
func addTimestamps(_ anArray:[Custom]) -> [Custom] {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "dd.mm.yyyy"

    anArray.forEach { (object) in
        let date = dateFormatter.date(from:object.date)
        let dateFromString = date!.timeIntervalSince1970
        object.timeStamp = dateFromString // "Cannot assign to property: 'object' is a 'let' constant"
    }
    return anArray
}

But Xcode returns a Swift Compile Error "Cannot assign to property: 'object' is a 'let' constant".

Why is object a let constant? Every object is of a Custom type, which I have defined a struct for, and every property I have defined with var. So I don't understand why I cannot assign a new value to a property (timeStamp or any other) of anArray. The code works fine when applied to an array literal for testing. What's different here?

The variables in your function are automatically let constants:

func addTimestamps(_ anArray:[Custom]) -> [Custom] {
                  // ^^^^^^^ this is a let
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "dd.mm.yyyy"

    anArray.forEach { (object) in
                    // ^^^^^^ this is also a let
        let date = dateFormatter.date(from:object.date)
        let dateFromString = date!.timeIntervalSince1970
        object.timeStamp = dateFromString // "Cannot assign to property: 'object' is a 'let' constant"
    }
    return anArray
}

I'd rewrite this use map:

func addTimestamps(_ anArray:[Custom]) -> [Custom] {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "dd.mm.yyyy"

    return anArray.map { object in
        var copy = object
        copy.timeStamp = dateFormatter.date(from: object.date)!.timeIntervalSince1970
        return copy
    }
}
1 Like

Actually, I wouldn't try to coerce timeStampinto a formatted string at all; I would store it as a Date and only rendering using a foratter when it actually needed displaying.

Since the JSON file doesn't have real timestamps you should probably ignore the timestamp from the file. I would set the timestamp in the init(from: Decoder) method for your struct. You probably don't have an init method, relying on the Decoder to all the work for you but it's simple enough to write for a struct like this.

That is not what the function is doing. The function should read from the date property (a String given as "dd.mm.yyyy") and overwrite the timeStamp property (a TimeInterval).

struct Custom: Hashable, Codable, Identifiable {
//let id = UUID()
let id: Int
let date: String
let year: Int
let month: Int
let day: Int
var recipient: String
var description: String
var amount: Float
var category: String
var timeStamp: TimeInterval
}

My intention is to have a variable to sort by. I want to sort by date and use the timeStamp variable for that.

Back to the problem – my mistake was that I was trying to alter the function parameter. OK, looking at the code by @mayoff, it was new to me that the array that .map is used on can accept transformed copies of its elements rather than transformations of its own elements.

I tried the following, thinking that copying anArray first, avoids the problem of trying to alter the parameter array handed to the function. But again, I cannot assign to the timeStamp property in the .map closure. Can you explain why not, @mayoff?

func addTimestamps(_ anArray:[Custom]) -> [Custom] {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "dd.mm.yyyy"

    let copy = anArray
    
    return copy.map { object in
        object.timeStamp = dateFormatter.date(from: object.date)!.timeIntervalSince1970
    }
}

The formal parameters of a closure, like the closure you pass to map, are automatically let constants.

To load the JSON data into an anArray, I am using the same code that Apple supplies in the Building Lists and Navigation tutorial.

The idea to let the objects have an initial time stamp to overwrite later came after I failed to create an init in the struct together with a default value for timeStamp in the struct. I tried the following, after reading Working with JSON in Swift, but that returns a runtime error.

init extension
extension Custom {
    init?(json: [String: Any]) {
        guard
            let id = json["id"] as? Int,
            let date = json["date"] as? String,
            let year = json["year"] as? Int,
            let month = json["month"] as? Int,
            let day = json["day"] as? Int,
            let recipient = json["recipient"] as? String,
            let description = json["description"] as? String,
            let amount = json["amount"] as? Float,
            let category = json["category"] as? String
            else {
                return nil
        }
            self.id = id
            self.date = date
            self.year = year
            self.month = month
            self.day = day
            self.recipient = recipient
            self.description = description
            self.amount = amount
            self.category = category
    }
}

Can you recommend a source to read up on init(from: Decoder)?

You seem somewhat confused about struct. If say, you have an Int and want to mutate only the third bit, you’re still essentially mutating the entire Int.

The same goes for struct. Even if you only mutate timeStamp, you’re essentially mutating the entire struct.

That’s why when you try to mutate object.timeStamp, not only you need timeStamp to be mutable (var), but also object.

The compiler is telling you that object is not mutable (because it’s a non-inout function parameter).

You can create a mutable copy of object by writing

var newVariable = object

You can even use the same name, which is a common shadowing is such scenario

var object = object

Keep in mind that this is a copy and has nothing to do with the original object.

Then you can mutate the newVariable and return it from the map.

That one is somewhat dated.

Try Encoding and Decoding Custom Types.

Edit:

It’s notable that the mutation of instance variable won’t mutate the instance itself if your instance is a class (as oppose to struct).

Classes behaves like a pointer in C. Even if you change the value the pointer is pointing to, you don’t change the value of pointer itself.

In this case it’s better to use a struct since your structure seems to be data container of some kind.

1 Like

The structure should reflect how you want to work in Swift side as much as possible.

In this case you’re computing a time interval from 1970 to date, which would be much easier if date is Data rather than String.

JSONDecoder is smart enough to do that for you. It also support different formats via dateDecodingStrategy.

1 Like

This is wrong btw. Correct is "dd.MM.yyyy"

Correct is "dd.MM.yyyy"

It is, alas, worse than that. If you want to guarantee a fixed-format date, you have to pin the locale to en_US_POSIX. See QA1480 NSDateFormatter and Internet Dates for the background on this.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

1 Like

Thanks. I read about the POSIX locale under "Working With Fixed Format Date Representations" in the Developer Documentation for DateFormatter. That page also includes the link you gave. Accordingly, I added these two lines:

dateFormatter.locale = Locale(identifier: "de_DE_POSIX")
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)

I forgot to mention, but once you change date to be of type Date, you can just use date instead of timeStamp since Date is Comparable.

1 Like

Accordingly, I added these two lines:

I specifically recommend en_US_POSIX for fixed-format dates; I’m not actually sure whether we guarantee to preserve the format of other POSIX locales.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple