Hello all, I am working on a project and have a view model that contains a list of items. These items are immutable (structs) and contain a list of other (immutable–struct) objects. The image describes this better.
Here is my models:
public struct GoogleFont: Hashable, Codable, Identifiable {
public struct Variant: Hashable, Codable, Identifiable {
public var id: String { name }
public let family: GoogleFont
public let name: String
var fileURL: String
var fontData: Data?
var font: Font?
public func fontWithSize(_ size: CGFloat) -> Font? {
...
}
public init(family: GoogleFont, name: String, fileURL: String) {
...
}
private enum Keys: String, CodingKey {
...
}
public init(from decoder: any Decoder) throws {
...
}
public func encode(to encoder: any Encoder) throws {
...
}
}
/// The name of the primary variant
public static let primaryVariantName = "regular"
public var id: String { name }
public let name: String
public let category: String
public var variants: [GoogleFont.Variant] = []
public init(name: String, category: String, variants: [GoogleFont.Variant]) {
...
}
public init(name: String, category: String, variantFilePairs: [[String]]) {
...
}
private enum Keys: String, CodingKey {
...
}
public init(from decoder: any Decoder) throws {
...
}
public func encode(to encoder: any Encoder) throws {
...
}
public func getVariant(named name: String) -> Variant? {
variants.first { variant in
variant.name == name
}
}
public func primaryVariant() -> Variant? {
getVariant(named: GoogleFont.primaryVariantName)
}
....
}
And here is some of my GoogleFontManager:
@Observable
public class GoogleFontManager {
private var apiKey: String
public var selectedFont: GoogleFont?
public var familySearchTerm = "Sacramento"
public var categoryFilter = Set(GoogleFontCategory.allCases)
public var sort: GoogleFontSort = .trending
public var allFonts: [GoogleFont] = []
public init(apiKey: String) {
...
}
/// Loads populates the `allFonts` array with fonts from an API request
@MainActor
public func loadFonts() async throws {
let endpoint = "https://www.googleapis.com/webfonts/v1/webfonts"
var parameters = ["key" : apiKey]
parameters["sort"] = sort.rawValue
if !familySearchTerm.isEmpty {
parameters["family"] = familySearchTerm
}
let fontsData = try await RequestHelper.getData(endpoint, parameters: parameters)
var fonts = try GoogleFont.decodeFonts(from: fontsData)
fonts = filterFonts(fonts)
self.allFonts = fonts
}
/// Filters the `allFonts` array based on the set of filters
public func filterFonts(_ fonts: [GoogleFont]) -> [GoogleFont] {
...
}
/// Populates the `fontData` of each font's primary variant
@MainActor
public func loadPrimaryVariantsForAllFonts(size: CGFloat = UIFont.labelFontSize) async throws {
if allFonts.isEmpty { return }
// create a task group that will get all primary variants at the same time
try await withThrowingTaskGroup(of: (fontIndex: Int, primaryVariantIndex: Int, fontData: Data).self) { group in
// enter a for loop that adds a task to get primary variant for each font
for fontIndex in allFonts.indices {
let font = allFonts[fontIndex]
// get primary variant name and index in variants list
guard let primaryVariant = font.primaryVariant(),
let primaryVariantIndex = font.variants.firstIndex(where: {
$0 == primaryVariant
}) else { break }
let variantURL = font.variants[primaryVariantIndex].fileURL
group.addTask {
// Get the font variant's `fontData`
return (fontIndex: fontIndex,
primaryVariantIndex: Int(primaryVariantIndex),
fontData: try await RequestHelper.getData(variantURL))
}
}
// Now the tasks have been created run the tasks asynchronousely and set font data for the fonts
for try await variantDataPair in group {
allFonts[variantDataPair.fontIndex].variants[variantDataPair.primaryVariantIndex].fontData = variantDataPair.fontData
allFonts[variantDataPair.fontIndex].variants[variantDataPair.primaryVariantIndex].font = try Font(from: variantDataPair.fontData, size: size)
}
}
}
/// ERRORS HERE \\\
/// Retrieve all font's variants
@MainActor
public func loadAllVariantsForSelectedFont(_ size: CGFloat = UIFont.labelFontSize, alwaysLoad: Bool = false) async throws {
// Unwrap selected font
guard let selectedFont = selectedFont else { return }
// Load each variant... in a task group
try await withThrowingTaskGroup(of: (variantIndex: Int, variantData: Data).self) { group in
for variantIndex in selectedFont.variants.indices {
// Access variant
let variant = selectedFont.variants[variantIndex]
// Make sue it needs to be loaded
guard alwaysLoad || variant.fontData != nil else { continue }
// Load it async
group.addTask {
return (variantIndex: Int(variantIndex),
variantData: try await RequestHelper.getData(variant.fileURL))
}
}
// Set data and fonts for the fonts
for try await variantDataPair in group {
self.selectedFont?.variants[variantDataPair.variantIndex].fontData = variantDataPair.variantData
self.selectedFont?.variants[variantDataPair.variantIndex].font = try Font(from: variantDataPair.variantData, size: size)
}
}
}
}
The problem
I am getting an error in the line group.addTask {
in the loadAllVariantsForSelectedFont
function.
The error is Main actor-isolated value of type '() async throws -> (variantIndex: Int, variantData: Data)' passed as a strongly transferred parameter; later accesses could race
I am seriously lost with this, and have no clue on how to fix it, and would so appreciate anybody’s thoughts about why it may be happening. What’s more puzzling is the same error does not happen on the loadPrimaryVariantsForAllFonts
function’s group.addTask {
line; both methods are pretty much the same.