Call an async function in an initializer

Hi,

I write an async decode function for parsing a JSON file. The async decode function will be called in a view model initializer. However, the error "Mutation of captured parameter 'self' in concurrently-executing code" pops up in the initializer. The code snippet is below.

struct CollectibleSet<CollectibleType> where CollectibleType: CollectibleObject {
    private(set) var allItems: [CollectibleType]
    private var collectedItems: [CollectibleType] {
        allItems.filter { $0.isCollected }
    }
        
    init(jsonFileName: String) {
        let loaderAndDecoder = LoadAndDecodedJsonData<CollectibleType>()
        let data = loaderAndDecoder.loadlocaljsonData(from: jsonFileName)
        Task {
            self.allItems = try! await loaderAndDecoder.decodeJsonData(from: data!)
            <<<<< The error pops up here >>>>>
        }
    }
}

Does anyone know how this issue can be solved? I have tried to solve this issue in many ways but still doesn't work. Any comments are appreciated.

Thanks in advance.

Would a static initializer work instead?

public static func staticInit(jsonFileName: String) async throws → Self {
    let data = try await ...
    return Self.init(...)
}

Also, as a side note, what's your purpose for wrapping an array in a generic-typed struct? You might want to try decoding directly to [CollectibleType]. Generally I'd avoid doing this kind of work in an actual initializer...

Why can't you make your initializer async?

struct CollectibleSet<CollectibleType> where CollectibleType: CollectibleObject {
    private(set) var allItems: [CollectibleType]
    private var collectedItems: [CollectibleType] {
        allItems.filter { $0.isCollected }
    }
        
    init(jsonFileName: String) async throws {
        let loaderAndDecoder = LoadAndDecodedJsonData<CollectibleType>()
        let data = loaderAndDecoder.loadlocaljsonData(from: jsonFileName)
        self.allItems = try await loaderAndDecoder.decodeJsonData(from: data!)
    }
}

Hi Helenurm,

Yes, your assumption is correct. I declare a decode function that can decode a JSON file and store the data in an array of a generic type 'CollectibleType'. So once I give a CollectibleType that corresponding to the json file's content. the function will output a corresponding array with that type.

How you usually do. if you avoid doing this kind of work in an actual initializer.

I think you're adding an unnecessary step for yourself! Also, why is decodeJsonData async? I would personally try to structure data flow in this way: Load Data object (async if necessary), synchronously decode the data (the helper below), then use this decoded result as a parameter in an initializer, for example...

/// This isolates the whole decoding process to be generic, and self-contained,
/// and not introduce an extra `struct`
func decodeFromJSON<D: Decodable>(_ data: Data) throws -> D {
    try JSONDecoder().decode(D.self, from: data)
}

/// Load data
let data: Data = try await loadDataFromSomewhere()
/// Type `D` for `decodeFromJSON` is inferred from `collectibleSet`'s type
let collectibleSet: [CollectibleType] = try decodeFromJSON(data)

/// Here's where that [CollectibleType] computed property lives,
//// now that we don't have a `struct CollectibleSet`
extension [CollectibleType] {
    /// try using a KeyPath in `.filter`, it's a really useful tool,
    //// especially with "KeyPath composition"...
    var collectedItems: Self { self.filter(\.isCollected) }
}

let onlyCollectedItems = collectibleSet.collectedItems

Hope this helps! If you have more questions, I'll do my best to answer :slight_smile:

1 Like

Thanks so much! Your code snippet is pretty helpful.

I declare 'decodeFromJSON' and 'loadDataFromSomewhere' as two separate global functions so that I can use them in any model's initializer.

The only thing makes me confused is the generic type 'D' in func 'decodeFromJSON'. Theoretically, the properties in type 'D' should exactly be corresponding to the keys in the intended decoding JSON file. However, if I declare a computed property in the type 'D', such as 'isCollected', the decoder won't work anymore since ther is no 'isCollected' key in the JSON file.

I think that is the reason why you use extension syntax to add a new computed property in a type. But are you able to add 'collectedItems' property in [CollectibleType] directly?

I am a beginner and barely know how to use extension syntax in a data flow. Could you talk more details about it?

Thanks in advance.

First I'm going to give a few example structs and show how they chose which properties to encode and decode, then I'll talk a little about extensions and data flow after

Run this snippet and take a look at the output

import Foundation

/// Conforming to "Codable". Normally, when conforming to a protocol, you have to provide some custom conformance methods
/// If you use Codable as intended, it will *automatically synthesize* the conformance methods. As long as every property
/// that you're asking it to decode is a primitive (string, bool, int, etc), *or* conforms to Codable itself.
/// Notice that everything here is a primitive or conforms to Codable (SomeProperty, MyCodableObject)

struct SomeProperty: Codable {
	let metaData: String
	let identifier: UUID
}

struct MyCodableObject: Codable {
	let id: Int
	let title: String
	let isNew: Bool
	let metaData: [SomeProperty]
	var someValue: Int = 3  // because this isn't in coding keys, it won't be decoded

	// `CodingKeys` is a special enum that lets the decode us from different key names than our property names
	// these are the *only* field names that it looks for.
	// `SomeProperty` didn't have coding keys, so it looks for `metaData` and `identifier` as keys
	enum CodingKeys: String, CodingKey {
		case id  // key is called "id"
		case title  // key is called "title"
		case isNew = "is_new"  // key is called "is_new" instead of "isNew" like our actual `let`
		case metaData = "meta_data_inf"
	}
}

/// Look into "multiline strings" in Swift; in my opinion they're super useful for
/// complex console log messages, also for holding test JSON data for API calls
let jsonData = """
  {
	"id": 3,
	"title": "Title",
	"is_new": true,
	"meta_data_inf": [
		{ "metaData": "some extra data", "identifier": "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"},
		{ "metaData": "more extra data", "identifier": "F621E1F8-C36C-495A-93FC-0C247A3E6FFF"}
	]
  }
"""
/// Get that string as utf8 data
let data = jsonData.data(using: .utf8)!

/// Same decode as before
func decodeFromJSON<D: Decodable>(_ data: Data) throws -> D {
	try JSONDecoder().decode(D.self, from: data)
}

/// Let's try decoding this JSON
let myObject: MyCodableObject = try decodeFromJSON(data)
print(myObject)

/// `Codable = Encodable & Decodable`, you're actually conforming to TWO separate protocols
/// you can choose to have an object only conform to one, but there's rarely a cost involved
/// in just having that object be `Codable` instead
let reEncodedString: String = String(decoding: try JSONEncoder().encode(myObject), as: UTF8.self)

// Notice that `someValue` wasn't encoded here; not in "CodingKeys!"
print(reEncodedString)

On that line, where I say let myObject: MyCodableObject = try decodeFromJSON(data), that generic parameter D is replaced with MyCodableObject behind the scenes; decodeFromJSON gets "specialized" and when I use it at the call site, it's actually calling a method with the signature decodeFromJSON(_ data: Data) throws → MyCodableObject.
The generic D: Decodable parameter just means you can use this function on anything Decodable, but when it actually runs on a specific model in code, it's no longer "generic," it's using the actual struct models you wrote.

Last rant! extension & computed vars are your best friends for organizing your code. It means you can split the definition of the model, and the behavior that that model can support. (You can even put them in different files, if your models are huge)

struct SensorData {
  var kelvinTemperature: Double
  var humidity: Double
}


extension SensorData {
  // note that "celsiusTemperature" isn't exactly a "property" of our sensor data, 
  // however it's deterministic based on the state of `SensorData`, which means
  // we should almost certainly put it in a computed variable
  var celsiusTemperature: Double { kelvinTemperature - 273 }

  // use other computed properties to bootstrap further useful derived behavior
  var fahrenheitTemperature: Double { celsiusTemperature * 9 / 5 + 32)
  var temperatureIsUnsafe: Bool { fahrenheitTemperature > 220 }
}

All I mean by "data flow" is "how values move through your program." You receive some JSON string from your API, you decode it into a model object, you do some data processing on that model, then you pass it along to your UI to be displayed.

The "rule of thumb" is: "Only let data pass through if it's necessary." For example, if you're using just the MyCodableObject.isNew property in your UI (displaying some fancy visual banner if isNew == true), you should only pass that Bool into your UI-layer: don't pass the whole MyCodableObject, because it holds a lot of unnecessary extra information (id, title, etc).
Don't be afraid of making a lot of custom structs and enums: this is how you're supposed to use the language!

Look into the concept of "domain modeling." Swift is really good at letting you do good domain modeling without much effort... Have a 5-star rating component? Model the state of it as an enum with a case for each possible star. This means that you never have to worry about some edge-case where rating = -1 stars (if you model rating as an Integer, technically it could hold any integer value...). This example is a little contrived, but hopefully it gets you thinking...

Here's a good meta-principle: try to make every helper have one specific responsibility. Your init method had many responsibilities: loading data async, then decoding it, then using it in init... this is hard to read/understand, which makes it hard to debug when something doesn't work.

Here's how you can simplify things visually with extensions! (Remember, you can extend almost any type... there are some restrictions, but as you encounter problems, just remember they are edge cases) For a helper method that has some input argument, you can turn it into an extension on that argument's Type without an input argument (self takes its place) :

let data: Data

// version 1
func decodeFromJSON<D: Decodable>(_ data: Data) throws -> D {
	try JSONDecoder().decode(D.self, from: data)
}

// version 2, equivalent in every way except "the syntax at the call site"
extension Data {
	func decodeFromJSON<D: Decodable>() throws -> D {
		try JSONDecoder().decode(D.self, from: self)
	}
}

// to me this is slightly hard to read
let object1: Something = try decodeFromJSON(data: data)
// to me, this is *much* simpler to read in my head
let object2: Something = try data.decodeFromJSON()

Reducing the burden of reading complex code is the key to "software architecture." Giving your software architecture is more like cleaning a house than "building something from the ground up": if everything looks visually messy, your brain can't naturally find patterns of meta-organization. Once you take all the garbage out, and put the dishes in the kitchen, the clothes in a big pile in the bedroom: you will naturally just "see" the next step (as long as you consciously keep trying to clean).
Maybe, once you put all your API call helpers in a single place, you will notice that you could have a single function handle all of their cases!

Software is about building something functional, and architecture is about using your programming language/tools to get that same functionality, but "safer" in some way (and more testable, and more readable preferably). Start by building the functionality, then learn language features one-by-one and try to understand how they can make your existing software better! In my humble opinion, your first architecture should be a collection of helper functions, isolated to extensions if possible (a language feature for " naturally grouping" things that use a common underlying type... these Types are the bedroom your clothes go in, or the kitchen that the dishes go in)