What to do when a model needs to use the network

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!

You don’t. You would make some kinda class to do the networking, and your model is only there for being the type that’s decoded

I’m not at a computer right now to look at this more in depth, but can you give more context about the view that’s showing all these fonts?

Thanks so much Mikaela, I have extracted the networking into its own manager and reformatted my program.