Hi, I am trying to use the Google Fonts API to implement a Google Fonts picker for my iOS app and have completed the base of the model (with all the properties and codable conformances etc.) however am having difficulty with how to retrieve and create fonts asynchronously within the model, and just the general best practice for what to do when a model relies on making network calls.
This is my model:
public struct GoogleFont: Hashable, Codable, Sendable {
public struct Variant: Hashable, Codable, Sendable, Identifiable {
/// ID of the variant, denoted as _family name_/_variant name_
public var id: String { "\(family.familyName)/\(variantName)" }
/// Family that the variant belongs to
public let family: GoogleFont
/// NAme of the variant, for examle, "400" or "700italic"
public let variantName: String
/// Size of the font used when creating font instances
public var size: CGFloat
/// URL where the font file can be found
private var fileURL: String
/// Font data retrieved from the URL. This is only loaded when the font is retrieved by calling `loadFont()`
private var fontData: Data?
/// the Font instance creasted from the font data.
public var font: Font?
public mutating func loadFont() async throws -> Font {
guard font == nil else { return font! }
fontData = try await RequestHelper.getData(fileURL)
font = try Font(from: fontData!, size: size)
}
public init(family: GoogleFont, size: CGFloat, variantName: String, fileURL: String) {
self.family = family
self.variantName = variantName
self.size = size
self.fileURL = fileURL
}
private enum Keys: String, CodingKey {
case family, variantName, fontData, size, fileURL
}
public init(from decoder: any Decoder) throws {
let cnt = try decoder.container(keyedBy: Keys.self)
family = try cnt.decode(GoogleFont.self, forKey: .family)
variantName = try cnt.decode(String.self, forKey: .variantName)
size = try cnt.decode(CGFloat.self, forKey: .size)
fileURL = try cnt.decode(String.self, forKey: .fileURL)
fontData = try cnt.decodeIfPresent(Data.self, forKey: .fontData)
if let data = fontData {
font = try Font(from: data, size: size)
}
}
public func encode(to encoder: any Encoder) throws {
try family.encode(to: encoder)
try variantName.encode(to: encoder)
try size.encode(to: encoder)
try fileURL.encode(to: encoder)
try fontData.encode(to: encoder)
}
}
public let familyName: String
public let category: String
public var variants: [GoogleFont.Variant] = []
private var variantFilePairs: [[String]]
public init(familyName: String, category: String, variants: [GoogleFont.Variant], variantFilePairs: [[String]]) {
self.familyName = familyName
self.category = category
self.variants = variants
self.variantFilePairs = variantFilePairs
}
private enum Keys: String, CodingKey {
case family, variants, files, category
}
public init(from decoder: any Decoder) throws {
// Create container
let cnt = try decoder.container(keyedBy: Keys.self)
// Decode root properties
familyName = try cnt.decode(String.self, forKey: .family)
category = try cnt.decode(String.self, forKey: .category)
// Decode the `variants` & `files` into a dictionary of files with key=name & value=URI
let variantFilePairsDict = try cnt.decode([String:String].self, forKey: .files)
variantFilePairs = variantFilePairsDict.map { (key, value) in
return [key, value]
}
}
public func encode(to encoder: any Encoder) throws {
try familyName.encode(to: encoder)
try category.encode(to: encoder)
try variants.encode(to: encoder)
}
public mutating func loadAllVariants(withSize size: CGFloat) async throws {
self.variants.removeAll()
self.variants = try await withThrowingTaskGroup(of: Variant.self) { group in
var variants = [Variant]()
for variantFile in variantFilePairs {
group.addTask {
var v = Variant(family: self, size: size, variantName: variantFile[0], fileURL: variantFile[1])
try await v.loadFont()
}
}
for try await variant in group {
variants.append(variant)
}
return variants
}
}
}
I have a GoogleFont with a Variant inside.
GoogleFont.Variant
On initialization
All properties are initialized normally, just without any data or a font instance yet.
On initialization from a decoder
All properties are decoded, and the data is, if it can be, i.e. if fontData is not nil, decode it and initialize the font too.
Accessing the font:
The loadFont()
method has to be called on it from an asynchronous environment which makes a network request for the data and sets it as the fontData, and then creates a Font instance from it. It uses a custom initializer that is beside the point.
GoogleFont
On initialization from decoder
It will only ever be initialized from JSON
Properties are initialized including a 2D array [variantName , variantFileURL]. Plus, a variants list is created empty.
Accessing font:
All variants have to be loaded by calling loadAllVariants(withSize:)
which populates the list asynchronously.
The problem:
I am overall confused about how I can mix my model with the networking calls. I feel I am doing it wrong when I start using Requests in a model.
Overall, I just want some way of retrieving Google fonts asynchronously so I can use them in a View with .font(_)
Please could anyone point me in the right direction, thank you if you can!