Загрузка нескольких изображений в CoreData

#ios #swift #iphone #core-data #moya

#iOS #swift #iPhone #core-данные #моя

Вопрос:

Я пишу приложение для iOS на Swift.

На моей домашней странице (HomeLandingViewController.swift) я должен параллельно вызвать два API, которые выдают мне список изображений, и я должен загрузить все эти изображения, а затем сбросить в CoreData. Пока этот процесс не завершится, я должен показать анимацию загрузки и т.д. В пользовательском интерфейсе.

ПОТОК:

Загрузка домашней страницы VC> Запуск анимации > Вызов API 1 и параллельный вызов API 2 > Получение массивов изображений из API 1 и API 2 > получение ДАННЫХ всех этих изображений > Сброс в Coredata > Уведомление домашней страницы VC о выполнении работы > Остановка анимации

Для этой цели я создал специальный класс (IconsHelper.swift)

Я использую сетевую библиотеку Moya.

Проблема в том, что все работает не так, как ожидалось. Поскольку все работает асинхронно, домашняя страница VC получает уведомления еще до загрузки изображений.

Мои фрагменты кода:

 IconsHelper.shared.getNewIconsFromServer()

class IconsHelper {
    static let shared: IconsHelper = .init()
    
    var group:DispatchGroup?

    //Get Icons from API 1 and API 2:
    func getNewIconsFromServer() {
        group = DispatchGroup()
        group?.enter()
        
        let dispatchQueue_amc = DispatchQueue(label: "BackgroundIconsFetch_AMC", qos: .background)
        
        dispatchQueue_amc.async(group: group) {
            self.getNewIcons(type: .amcIcons)
        }
        
        if group?.hasGroupValue() ?? false {
            group?.leave()
            Log.b("CMSIcons: Group Leave 1")
        }
        
        group?.enter()
        
        let dispatchQueue_bank = DispatchQueue(label: "BackgroundIconsFetch_Bank", qos: .background)
        dispatchQueue_bank.async(group: group) {
            self.getNewIcons(type: .bankIcons)
        }
        
        if group?.hasGroupValue() ?? false {
            group?.leave()
            Log.b("CMSIcons: Group Leave 2")
        }
        
        group?.notify(queue: .global(), execute: {
            Log.b("CMSIcons: All icons fetched from server.")
        })
    }

    func getNewIcons(type: CMSIconsTypes) {
        let iconsCancellableToken: CancellableToken?
        
        let progressClosure: ProgressBlock = { response in
        }
        
        let activityChange: (_ change: NetworkActivityChangeType) -> Void = { (activity) in
            
        }
        
        let cmsCommonRequestType=self.getCmsCommonRequestType(type: type)
        
        iconsCancellableToken = CMSProvider<CMSCommonResponse>.request( .cmsCommonRequest(request: cmsCommonRequestType), success: { (_response) in
            Log.b("CMSIcons: Get new icons from server for type: (type)")
            
            //Set http to https:
            var iconsHostname:String=""{
                didSet {
                    if let comps=URLComponents(string: iconsHostname) {
                        var _comps=comps
                        _comps.scheme = "https"
                        if let https = _comps.string {
                            iconsHostname=https
                        }
                    }
                }
            }
            
            if (_response.data?.properties != nil) {
                if _response.status {
                    
                    let alias = self.getCmsAlias(type: type)
                    let property = _response.data?.properties.filter {$0.alias?.lowercased()==ValueHelper.getCMSAlias(alias)}.first?.value
                    
                    if let jsonStr = property {
                        iconsHostname = _response.data?.hostName ?? ""
                        
                        if let obj:CMSValuesResponse = CMSValuesResponse.map(JSONString: jsonStr) {
                            
                            if let fieldsets=obj.fieldsets {
                                if fieldsets.count > 0 {
                                    for index in 1...fieldsets.count {
                                        let element=fieldsets[index-1]
                                        if let prop = element.properties {
                                            if(prop.count > 0) {
                                                let urlAlias = self.getCmsURLAlias(type: type)
                                                
                                                let iconUrl = prop.filter {$0.alias?.lowercased()==ValueHelper.getCMSAlias(urlAlias)}.first?.value
                                                
                                                let name = prop.filter {$0.alias?.lowercased()==ValueHelper.getCMSAlias(.iconsNameAlias)}.first?.value
                                                
                                                if let iconUrl=iconUrl, let name=name {
                                                    if let url = URL(string: iconsHostname iconUrl) {
                                                        DispatchQueue.global().async {
                                                            if let data = try? Data(contentsOf: url) {
                                                                Log.b("CMSIcons: Icon url (url.absoluteString) to Data done.")
                                                                var databaseDumpObject=CMSIconStructure()
                                                                databaseDumpObject.name=name
                                                                databaseDumpObject.data=data
                                                                self.dumpIconToLocalStorage(object: databaseDumpObject, type: type)
                                                                
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }//Loop ends.
                                    //After success:
                                    self.setFetchIconsDateStamp(type:type)
                                }
                            }
                        }
                    }
                }
            }
            
        }, error: { (error) in
            
        }, failure: { (_) in
            
        }, progress: progressClosure, activity: activityChange) as? CancellableToken
        
    }

    //Dump icon data into CoreData:
    func dumpIconToLocalStorage(object: CMSIconStructure, type: CMSIconsTypes) {
        let entityName =  self.getEntityName(type: type)
        
        if #available(iOS 10.0, *) {
            Log.b("Do CoreData task in background thread")
            //Do CoreData task in background thread:
            
            let context = appDelegate().persistentContainer.viewContext
            let privateContext: NSManagedObjectContext = {
                let moc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
                moc.parent = context
                return moc
            }()
            
            //1: Read all offline Icons:
            let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
            
            fetchRequest.predicate = NSPredicate(format: "name = %@",
                                                 argumentArray: [object.name.lowercased()])
            
            do {
                let results = try privateContext.fetch(fetchRequest) as? [NSManagedObject]
                if results?.count != 0 {
                    //2: Icon already found in CoreData:
                    
                    if let icon=results?[0] {
                        icon.setValue(object.name.lowercased(), forKey: "name") //save lowercased
                        icon.setValue(object.data, forKey: "data")
                    }
                    
                } else {
                    //3: Icon not found in CoreData:
                    
                    let entity = NSEntityDescription.entity(forEntityName: entityName, in: privateContext)
                    let newIcon = NSManagedObject(entity: entity!, insertInto: privateContext)
                    newIcon.setValue(object.name.lowercased(), forKey: "name") //save lowercased
                    newIcon.setValue(object.data, forKey: "data")
                    
                }
                
                Log.b("CMSIcons: Icon data saved locally against name: (object.name)")
                
            } catch {
                Log.i("Failed reading CoreData (entityName.uppercased()). Error: (error)")
            }
            privateContext.perform {
                // Code in here is now running "in the background" and can safely
                // do anything in privateContext.
                // This is where you will create your entities and save them.
                
                do {
                    try privateContext.save()
                } catch {
                    Log.i("Failed reading CoreData (entityName.uppercased()). Error: (error)")
                }
            }
        } else {
            // Fallback on earlier versions
        }
    }
}
  

Комментарии:

1. Не храните изображения в core data. Сохраните его на диск и сохраните путь вместо этого.

2. вы имеете в виду ошибки пользователя?

Ответ №1:

Как правило, не рекомендуется хранить изображения в виде двоичных данных в сохраняемом хранилище Core Data. Вместо этого запишите изображения в локальный каталог и сохраните локальный URL-адрес в core data. Вот пример рабочего процесса, который может упростить некоторые из ваших проблем, с которыми вы сталкиваетесь, используя этот рекомендуемый подход:

 class IconsHelper {
    
    let container: NSPersistentContainer
    let provider: CMSProvider<CMSCommonResponse>
    private let queue: DispatchQueue = DispatchQueue(label: "IconsHelper", qos: .userInitiated)
    
    let documentsDirectory: URL = {
        let searchPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
        guard let path = searchPath.last else {
            preconditionFailure("Unable to locate users documents directory.")
        }
        
        return URL(fileURLWithPath: path)
    }()
    
    init(container: NSPersistentContainer, provider: CMSProvider<CMSCommonResponse>) {
        self.container = container
        self.provider = provider
    }
    
    enum Icon: String, Hashable {
        case amc
        case bank
    }
    
    func getIcons(_ icons: Set<Icon>, dispatchQueue: DispatchQueue = .main, completion: @escaping ([Icon: URL]) -> Void) {
        queue.async {
            var results: [Icon: URL] = [:]
            
            guard icons.count > 0 else {
                dispatchQueue.async {
                    completion(results)
                }
                return
            }
            
            let numberOfIcons = icons.count
            var completedIcons: Int = 0
            
            for icon in icons {
                let request = [""] // Create request for the icon
                self.provider.request(request) { (result) in
                    switch result {
                    case .failure(let error):
                        // Do something with the error
                        print(error)
                        completedIcons  = 1
                    case .success(let response):
                        // Extract information from the response for the icon
                        
                        let imageData: Data = Data() // the image
                        let localURL = self.documentsDirectory.appendingPathComponent(icon.rawValue   ".png")
                        
                        do {
                            try imageData.write(to: localURL)
                            try self.storeURL(localURL, forIcon: icon)
                            results[icon] = localURL
                        } catch {
                            print(error)
                        }
                        
                        completedIcons  = 1
                        
                        if completedIcons == numberOfIcons {
                            dispatchQueue.async {
                                completion(results)
                            }
                        }
                    }
                }
            }
        }
    }
    
    func storeURL(_ url: URL, forIcon icon: Icon) throws {
        // Write the local URL for the specific icon to your Core Data Container.
        let context = container.newBackgroundContext()
        
        // Locate amp; modify, or create CMSIconStructure using the context above.
        
        try context.save()
    }
}
  

Затем в вашем контроллере просмотра домашней страницы:

 // Display Animation

let helper: IconsHelper = IconsHelper.init(container: /* NSPersistentContainer */, provider: /* API Provider */)
helper.getIcons([.amc, .bank]) { (results) in
    // Process Results
    // Hide Animation
}
  

Общий дизайн здесь заключается в том, чтобы иметь один вызов, который будет обрабатывать загрузку и обработку изображений, а затем отвечать результатами после завершения всех сетевых вызовов и взаимодействия с основными данными.

В примере вы инициализируете свой IconsHelper ссылкой на CoreData NSPersistentContainer и свой сетевой экземпляр. Помогает ли этот подход прояснить, почему ваш пример кода работает не так, как вы ожидаете?