#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 и свой сетевой экземпляр. Помогает ли этот подход прояснить, почему ваш пример кода работает не так, как вы ожидаете?