Текст книги "iOS. Приемы программирования"
Автор книги: Вандад Нахавандипур
Жанр: Зарубежная компьютерная литература, Зарубежная литература
сообщить о неприемлемом содержимом
Текущая страница: 51 (всего у книги 59 страниц)
16.8. Реализация отношений в Core Data
Постановка задачиНеобходимо иметь возможность связывать управляемые объекты друг с другом, например связать контакт Person с каталогом Home, в котором он находится.
РешениеПрименяйте в редакторе модели обратные отношения.
ОбсуждениеВ Core Data могут существовать следующие виды отношений: «один к одному» (one-to-one), обратное отношение «один ко многим» или обратное отношение «многие ко многим». Далее приведены жизненные примеры каждой разновидности отношений.
• Отношение «один к одному» – существует между человеком и его носом. У каждого человека может быть только один нос, и каждый нос может принадлежать только одному человеку.
• Обратное отношение «один ко многим» – существует между сотрудником и его менеджером. У сотрудника может быть только один непосредственный менеджер, но одному менеджеру могут одновременно подчиняться несколько сотрудников. В данном случае для сотрудника создается отношение «один к одному», однако для менеджера это отношение «один (менеджер) ко многим (сотрудникам)». Поэтому такое отношение и называется обратным.
• Обратное отношение «многие ко многим» – возникает между человеком и автомобилем. Одна машина может использоваться несколькими людьми, а один человек может пользоваться несколькими машинами.
В Core Data можно создавать отношения «один к одному», но я категорически не рекомендую этого делать. Возвращаясь к недавнему примеру с носом, необходимо отметить, что человек будет знать, чей нос торчит у него на лице, а вот нос не будет знать, кому он принадлежит. Обратите внимание на то, что эта система отношений «один к одному» отличается от взаимно однозначных отношений, с которыми вы могли столкнуться в других системах управления базами данных: объект A и объект B будут взаимосвязаны друг с другом, если между ними существует отношение «один к одному». В Core Data при отношении «один к одному» объект A будет знать, что связан с объектом B, но не наоборот. В объектно-ориентированном языке, таком как Objective-C, всегда лучше создавать обратные отношения, такие, которые позволяют дочерним элементам обращаться к родительским. При отношении «один ко многим» объект, который может быть ассоциирован с рядом других объектов, будет удерживать это множество объектов. Это будет множество типа NSSet. Хотя при отношениях «один к одному» оба объекта, состоящие в таких отношениях, сохраняют ссылку друг на друга, так как используют правильное имя класса «напарника», это отношение все равно принадлежит к типу «один к одному», и один объект может быть представлен в другом путем простого указания своего имени класса.
Итак, создадим такую модель данных, в которой используются преимущества обратного отношения «один ко многим».
1. Найдите в Xcode файл xcdatamodel, созданный системой в самом начале работы с проектом Core Data. Это было показано во введении к данной главе (создание такого проекта описано в разделе 16.1).
2. Откройте в редакторе файл модели данных, щелкнув на нем кнопкой мыши.
3. Удалите все созданные ранее сущности, выделяя их и нажимая клавишу Delete.
4. Создайте новую сущность и назовите ее Employee (Сотрудник). Создайте для этой сущности три атрибута, которые будут называться firstName (типа String), lastName (типа String) и age (типа Integer 32) (рис. 16.13).
Рис. 16.13. Сущность Employee с тремя атрибутами
5. Создайте сущность под названием Manager (Менеджер) с такими же атрибутами, как и у сущности Employee: firstName (типа String), lastName (типа String) и age (типа Integer 32) (рис. 16.14).
Рис. 16.14. Сущность Manager с тремя атрибутами
6. Создайте новое отношение для сущности Manager. Для этого сначала нужно выбрать данную сущность из списка, а потом нажать кнопку + в нижней части области Relationships (Отношения) (рис. 16.15).
Рис. 16.15. Добавление нового отношения к сущности Manager
7. В качестве имени нового отношения задайте employees (Сотрудники) (рис. 16.16).
Рис. 16.16. Изменение имени нового отношения типа «менеджер к сотрудникам»
8. Выберите сущность Employee и создайте для нее новое отношение. Назовите это отношение manager (рис. 16.17).
Рис. 16.17. Изменение имени нового отношения между сотрудниками и менеджером
9. Выберите сущность Manager, а потом выделите отношение employees для Manager. В области Relationships (Отношения) выберите параметр Employee (Сотрудник) в раскрывающемся меню Destination (Назначение). Именно так – ведь в этом отношении мы хотим соединить сущности Manager и Employee. В столбце Inverse (Обратные отношения) укажите значение manager (так как отношение manager будет связывать сотрудника (Employee) с менеджером (Manager)). Наконец, установите флажок To-Many Relationship (Отношение ко многим) в инспекторе модели данных (см. раздел 16.1). Результаты приведены на рис. 16.18.
Рис. 16.18. Обратное отношение, установленное между менеджером и сотрудниками
10. Выделите обе сущности (Employee и Manager), выполните команду File – New File (Файл -Новый файл) и создайте классы управляемых объектов для вашей модели, как описано в разделе 16.2.
Создав обратное отношение «один ко многим», откройте. h-файл вашей сущности Employee:
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
@class Manager;
@interface Employee: NSManagedObject
@property (nonatomic, retain) NSNumber * age;
@property (nonatomic, retain) NSString * firstName;
@property (nonatomic, retain) NSString * lastName;
@property (nonatomic, retain) Manager *manager;
@end
Как видите, в этом файле появилось новое свойство. Оно называется manager и относится к типу Manager. Таким образом, начиная с данного момента мы при наличии ссылки на конкретный объект типа Employee можем получить доступ к свойству manager, а через него – к объекту Manager данного конкретного сотрудника (если менеджер есть). Рассмотрим. h-файл сущности Manager:
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
@class Employee;
@interface Manager: NSManagedObject
@property (nonatomic, retain) NSNumber * age;
@property (nonatomic, retain) NSString * firstName;
@property (nonatomic, retain) NSString * lastName;
@property (nonatomic, retain) NSSet *employees;
@end
@interface Manager (CoreDataGeneratedAccessors)
– (void)addFKManagerToEmployeesObject:(Employee *)value;
– (void)removeFKManagerToEmployeesObject:(Employee *)value;
– (void)addFKManagerToEmployees:(NSSet *)values;
– (void)removeFKManagerToEmployees:(NSSet *)values;
@end
Для сущности Manager также создается свойство employees. Тип данных этого объекта – NSSet. Это означает, что свойство employees любого экземпляра сущности Manager может содержать от 1 до N сущностей Employee. В этом и заключается принцип отношения «один ко многим»: один менеджер, несколько сотрудников.
Другой тип отношений, которые, возможно, потребуется реализовать, называется «многие ко многим». По сравнению с отношением Manager к Employee при отношении «многие ко многим» один менеджер может иметь N сотрудников, а каждый сотрудник может подчиняться N менеджерам. Чтобы организовать такие отношения, выполните те же инструкции, что и при создании отношения «один ко многим», но выделите сущность Employee, а потом отношение manager. Измените это название на managers и установите флажок To-Many Relationship (Отношение ко многим) (рис. 16.19). Теперь стрелка будет заострена с обоих концов.
Рис. 16.19. Создание отношения «многие ко многим» между сущностями Manager и Employee
Теперь, открыв файл Employee.h, вы увидите, что его содержимое изменилось:
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
@class Manager;
@interface Employee: NSManagedObject
@property (nonatomic, retain) NSNumber * age;
@property (nonatomic, retain) NSString * firstName;
@property (nonatomic, retain) NSString * lastName;
@property (nonatomic, retain) NSSet *managers;
@end
@interface Employee (CoreDataGeneratedAccessors)
– (void)addManagersObject:(Manager *)value;
– (void)removeManagersObject:(Manager *)value;
– (void)addManagers:(NSSet *)values;
– (void)removeManagers:(NSSet *)values;
@end
Как видите, свойство managers сущности Person теперь представляет собой множество. Поскольку отношение сотрудника к его менеджерам – это множество и такое же отношение существует между менеджером и сотрудниками, здесь мы имеем пример отношения «многие ко многим»
В коде, написанном для отношения «один ко многим», можно просто создать новый управляемый объект Manager (о том, как вставлять объекты в контекст управляемых объектов, рассказано в разделе 16.3), сохранить его в контексте управляемых объектов, а потом соединить с парой управляемых объектов Employee – и их тоже сохранить в контексте. Теперь, чтобы ассоциировать менеджера с сотрудником, задайте в качестве значения для свойства FKEmployeeToManager, относящегося к экземпляру Employee, экземпляр управляемого объекта Manager. После этого фреймворк Core Data сам создаст необходимое отношение.
Если потребуется получить всех сотрудников (типа Employee), ассоциированных с объектом-менеджером (типа Manager), нужно будет просто воспользоваться методом экземпляра allObjects, относящимся к свойству FKManagerToEmployees вашего объекта-менеджера. Это объект типа NSSet, поэтому можно применить метод экземпляра allObjects, чтобы получить массив всех объектов-сотрудников, ассоциированных с конкретным объектом-менеджером.
16.9. Выборка данных в фоновом режиме
Постановка задачиТребуется выполнять операции выборки данных в стеке Core Data, причем только в фоновом режиме. Это отличная возможность создать по-настоящему отзывчивый пользовательский интерфейс.
РешениеПеред тем как заниматься выборкой данных в фоновом режиме, создайте новый контекст управляемых объектов с параллелизмом типа NSPrivateQueueConcurrencyType. Затем воспользуйтесь методом performBlock: нового фонового контекста для выборки данных в фоновом режиме. Как только это будет сделано и вы будете готовы использовать выбранные объекты в пользовательском интерфейсе, вернитесь в поток пользовательского интерфейса с помощью dispatch_async (см. раздел 7.4). Далее для каждого объекта, выбранного в фоновом режиме, выполните в основном контексте метод objectWithID:. Так объекты, выбранные в фоновом режиме, будут перенесены в ваш приоритетный контекст, где вы сможете оперировать ими в потоке пользовательского интерфейса.
ОбсуждениеВыборка объектов в основном потоке – не самая хорошая идея. Выполнять ее в главном потоке можно лишь в случаях, когда в стеке Core Data совсем немного элементов. Дело в том, что при операции выборки в Core Data обычно выполняется поисковый вызов. Затем этот вызов должен выбрать для вас определенные данные, обычно с помощью предиката. Чтобы сделать пользовательский интерфейс более отзывчивым, лучше всего выполнять такие операции выборки в фоновом контексте.
Вы можете создать в приложении столько контекстов, сколько захотите, однако помните об одном железном правиле. Нельзя передавать управляемые объекты между контекстами в разных потоках, так как объекты не являются потокобезопасными. Таким образом, если вы выбираете объекты в фоновом контексте, то не можете использовать их в главном потоке. Вот как следует передавать управляемые объекты между потоками: объект выбирается в фоновом потоке, а потом переносится в главный контекст (контекст, работающий в основном потоке). Это делается с помощью метода objectWithID: главного контекста. Этот метод принимает объект типа NSManagedObjectID. Поэтому в фоновом потоке мы на самом деле не выбираем управляемые объекты как таковые, а лишь берем их сохраняемые ID, после чего передаем эти ID главному контексту, который сам получает для вас управляемый объект. Итак, вы выполняете в фоновом режиме и поиск, и выборку объектов, затем передаете ID найденных объектов главному контексту. Получением самих объектов занимается уже главный контекст. Если действовать таким образом, главный контекст будет располагать сохраняемыми ID объектов, а получение этих объектов из постоянного хранилища в такой ситуации протекает гораздо быстрее, чем при выполнении полномасштабного поиска в главном контексте.
В этом разделе предполагается, что вы уже создали модель управляемых объектов Person. Подобная модель показана на рис. 16.20.
Рис. 16.20. Простая модель Core Data, используемая в этом разделе
При работе с этой моделью я заполню стек 1000 объектов Person, как показано в следующем коде, а уже потом попробую выбирать информацию из стека:
– (void) populateDatabase{
for (NSUInteger counter = 0; counter < 1000; counter++){
Person *person =
[NSEntityDescription
insertNewObjectForEntityForName: NSStringFromClass([Person class])
inManagedObjectContext: self.managedObjectContext];
person.firstName = [NSString stringWithFormat:@"First name %lu",
(unsigned long)counter];
person.lastName = [NSString stringWithFormat:@"Last name %lu",
(unsigned long)counter];
person.age = @(counter);
}
NSError *error = nil;
if ([self.managedObjectContext save:&error]){
NSLog(@"Managed to populate the database.");
} else {
NSLog(@"Failed to populate the database. Error = %@", error);
}
}
Обратите внимание: я использую класс NSStringFromClass для преобразования имени класса Person в строку и для последующего инстанцирования объектов такого типа. Некоторые программисты предпочитают типизировать Person как строковый литерал. Но если жестко запрограммировать вашу строку таким образом, может возникнуть проблема. Допустим, позже вы решите изменить имя Person в стеке Core Data, а жестко закодированная строка никуда не денется. Она может привести к аварийному завершению вашего приложения во время исполнения, так как объекта модели с именем Person больше не существует. Но если вы примените вышеупомянутую функцию для преобразования имени класса в обычную строку, то при изменении имени класса или отсутствии такого класса получите ошибку времени компиляции. Такие ошибки выявляются еще до ввода приложения в работу, и у вас будет время их исправить.
Прежде чем продолжать обсуждение, оговорюсь: предполагается, что вы уже заполнили базу данных с помощью последнего написанного нами метода. Далее в общих чертах изложено, как мы собираемся выполнять выборку в фоновом контексте.
1. Создаем фоновый контекст с помощью метода-инициализатора initWithConcurrencyType:, относящегося к классу NSManagedObjectContext, затем передаем этому методу значение NSPrivateQueueConcurrencyType. В результате получаем контекст, имеющий собственную закрытую очередь диспетчеризации. Поэтому, если вызвать в контексте блок performBlock:, этот блок будет выполнен в закрытой фоновой очереди.
2. Затем мы собираемся задать в фоновом контексте значение свойства persistentStoreCoordinator, которое будет равно экземпляру координатора нашего постоянного хранилища данных. Таким образом мы свяжем фоновый контекст с координатором постоянного хранилища. В результате, если выполнить выборку в фоновом контексте, эта операция позволит получить данные прямо с диска или из любого другого места, где их может хранить координатор.
3. Выполняем в фоновом контексте вызов performBlock:, а затем даем запрос на выборку. В рамках этого запроса требуется найти в стеке Core Data всех людей, чей возраст относится к диапазону от 100 до 200. Подчеркиваю: реалистичность эксперимента нас в данном случае не волнует. Я хочу лишь продемонстрировать, как действует выборка данных в фоновом режиме. Создавая запрос выборки данных, мы устанавливаем его свойство resultType в значение NSManagedObjectIDResultType. Так мы гарантируем, что результаты, возвращаемые после выполнения этого запроса на выборку, состоят не из управляемых объектов как таковых, а только из ID этих объектов. Как объяснялось ранее, мы не собираемся выбирать сами управляемые объекты, поскольку при выборке этих объектов в фоновом контексте не сможем использовать их в основном потоке. Итак, в фоновом контексте мы только выбираем их ID, а преобразуем эти ID в реальные управляемые объекты уже в главном контексте. После этого такие объекты можно использовать в основном потоке.
Вот как создается запрос на выборку:
– (NSFetchRequest *) newFetchRequest{
NSFetchRequest *request = [[NSFetchRequest alloc]
initWithEntityName:
NSStringFromClass([Person class])];
request.fetchBatchSize = 20;
request.predicate =
[NSPredicate predicateWithFormat:@"(age >= 100) AND (age <= 200)"];
request.resultType = NSManagedObjectIDResultType;
return request;
}
А вот как мы будем создавать фоновый контекст и выполнять в нем запрос на выборку данных:
– (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
__weak NSManagedObjectContext *mainContext = self.managedObjectContext;
__weak AppDelegate *weakSelf = self;
__block NSMutableArray *mutablePersons = nil;
/* Создаем фоновый контекст */
NSManagedObjectContext *backgroundContext =
[[NSManagedObjectContext alloc]
initWithConcurrencyType: NSPrivateQueueConcurrencyType];
backgroundContext.persistentStoreCoordinator =
self.persistentStoreCoordinator;
/* Выполняем блок в фоновом контексте */
[backgroundContext performBlock: ^{
NSError *error = nil;
NSArray *personIds = [backgroundContext
executeFetchRequest: [weakSelf newFetchRequest]
error:&error];
if (personIds!= nil && error == nil){
mutablePersons = [[NSMutableArray alloc]
initWithCapacity: personIds.count];
/* Теперь переходим в главный контекст и получаем эти объекты
в главном контексте, исходя из их ID */
dispatch_async(dispatch_get_main_queue(), ^{
for (NSManagedObjectID *personId in personIds){
Person *person = (Person *)[mainContext
objectWithID: personId];
[mutablePersons addObject: person];
}
[weakSelf processPersons: mutablePersons];
});
} else {
NSLog(@"Failed to execute the fetch request.");
}
}];
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Этот код собирает все управляемые объекты в виде массива, а затем вызывает в делегате нашего приложения метод processPersons:, обрабатывающий результаты массива. Напишем этот метод следующим образом:
– (void) processPersons:(NSArray *)paramPersons{
for (Person *person in paramPersons){
NSLog(@"First name = %@, last name = %@, age = %ld",
person.firstName,
person.lastName,
(long)person.age.integerValue);
}
}
Разделы 7.4, 16.4 и 16.6.
16.10. Использование специальных типов данных в модели Core Data
Постановка задачиВы считаете, что набор типов данных, представленных в Core Data, не удовлетворяет стоящим перед вами требованиям. Вам хотелось бы использовать в объектах моделей и дополнительные типы данных, например UIColor. Но такие объекты не содержатся в Core Data в готовом виде.
РешениеИспользуйте преобразуемые типы данных.
ОбсуждениеCore Data позволяет создавать для объектов моделей свойства, а потом присваивать этим свойствам типы данных. Выбор при этом довольно ограничен: Core Data допускает использование лишь таких типов данных, которые могут быть преобразованы в экземпляр NSData и обратно. Но существует целый ряд популярных классов, которые вы по умолчанию не можете использовать в таких свойствах. Что же делать? Применяйте преобразуемые свойства. Сначала поясню, что это такое.
Допустим, вы хотите создать в Core Data объект модели и назвать этот объект Laptop. У данного объекта будет два свойства: model типа String и color, которое должно относиться к типу UIColor. В Core Data не предоставляется такой тип данных, поэтому для его получения нам придется создать подкласс от NSValueTransformer. Назовем этот класс ColorTransformer. Вот что станем делать при его реализации.
1. Переопределим его метод класса allowsReverseTransformation и вернем от него значение YES. Так мы сообщим Core Data о возможности преобразования цветов в данные и обратно.
2. Переопределим метод transformedValueClass этого класса и возвратим от него имя класса NSData. Возвращаемое значение этого метода сообщает Core Data, в какой класс вы будете преобразовывать специальное значение. В данном случае происходит преобразование UIColor в NSData. Поэтому вы должны вернуть от этого метода имя класса NSData.
3. Переопределим метод экземпляра transformedValue:, относящийся к преобразователю. В нашем методе будем брать входящее значение (которое в данном случае будет экземпляром UIColor), преобразовывать его в NSData и возвращать эти данные.
4. Переопределим метод экземпляра reverseTransformedValue:, относящийся к преобразователю, и сделаем это с совершенно противоположной целью. Берем входящее значение (здесь – данные) и преобразуем его в цвет.
Имея всю эту информацию, продолжим реализацию преобразователя. Чтобы сохранять цвет как данные, просто разделим его на целочисленные компоненты, которые будут сохраняться в массиве:
#import <UIKit/UIKit.h>
#import «ColorTransformer.h»
@implementation ColorTransformer
+ (BOOL) allowsReverseTransformation{
return YES;
}
+ (Class) transformedValueClass{
return [NSData class];
}
– (id) transformedValue:(id)value{
/* Преобразуем цвет в данные */
UIColor *color = (UIColor *)value;
CGFloat red, green, blue, alpha;
[color getRed:&red green:&green blue:&blue alpha:&alpha];
CGFloat components[4] = {red, green, blue, alpha};
NSData *dataFromColors = [[NSData alloc] initWithBytes: components
length: sizeof(components)];
return dataFromColors;
}
– (id) reverseTransformedValue:(id)value{
/* Выполняем обратное преобразование из данных в цвет */
NSData *data = (NSData *)value;
CGFloat components[4] = {0.0f, 0.0f, 0.0f, 0.0f};
[data getBytes: components length: sizeof(components)];
UIColor *color = [UIColor colorWithRed: components[0]
green: components[1]
blue: components[2]
alpha: components[3]];
return color;
}
@end
Теперь возвращаемся к модели данных. Создадим управляемый объект Laptop и его атрибуты/свойства. Убедитесь, что атрибут color является преобразуемым. Выделив этот атрибут, нажмите на клавиатуре Alt+Command+3 и откройте Model Inspector (Инспектор модели) для этого атрибута. В поле name преобразуемого класса введите имя специального преобразователя. В данном случае это будет ColorTransformer (рис. 16.21).
Рис. 16.21. Создание модели с преобразуемым атрибутом
Теперь воспользуемся приемами, изученными в разделе 16.2, и сгенерируем файл класса для управляемого объекта Laptop. После этого перейдем к заголовочному файлу этого управляемого объекта. Как видите, атрибут color рассматриваемого класса относится к типу id:
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
@interface Laptop: NSManagedObject
@property (nonatomic, retain) NSString * model;
@property (nonatomic, retain) id color;
@end
Уже неплохо. Но чтобы сделать код еще лучше, в частности помочь компилятору выявлять возможные проблемы (они могут возникать, если присваивать этому свойству значения неподходящих типов), вручную изменим этот тип данных на UIColor:
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
/* Обязательно импортируем эту информацию в таком виде, в каком
UIColor находится в UIKit */
#import <UIKit/UIKit.h>
@interface Laptop: NSManagedObject
@property (nonatomic, retain) NSString * model;
@property (nonatomic, retain) UIColor *color;
@end
Итак, осталось объединить весь изученный материал и применить его на практике. В делегате нашего приложения создадим экземпляр Laptop и зададим для него красный цвет. Затем вставим этот объект в стек Core Data и попытаемся считать его обратно. Так мы проверим, удалось ли успешно сохранить цветовое значение и вновь достать его из базы данных:
#import «AppDelegate.h»
#import «Laptop.h»
@implementation AppDelegate
@synthesize managedObjectContext = _managedObjectContext;
@synthesize managedObjectModel = _managedObjectModel;
@synthesize persistentStoreCoordinator = _persistentStoreCoordinator;
– (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
/* Сначала сохраняем объект laptop с заданным цветом */
Laptop *laptop =
[NSEntityDescription
insertNewObjectForEntityForName: NSStringFromClass([Laptop class])
inManagedObjectContext: self.managedObjectContext];
laptop.model = @"model name";
laptop.color = [UIColor redColor];
NSError *error = nil;
if ([self.managedObjectContext save:&error] == NO){
NSLog(@"Failed to save the laptop. Error = %@", error);
}
/* Теперь находим этот же laptop */
NSFetchRequest *fetch =
[[NSFetchRequest alloc]
initWithEntityName: NSStringFromClass([Laptop class])];
fetch.fetchLimit = 1;
fetch.predicate = [NSPredicate predicateWithFormat:@"color == %@",
[UIColor redColor]];
error = nil;
NSArray *laptops = [self.managedObjectContext
executeFetchRequest: fetch
error:&error];
/* Проверка на 1, поскольку лимит выборки равен 1 */
if (laptops.count == 1 && error == nil){
Laptop *fetchedLaptop = laptops[0];
if ([fetchedLaptop.color isEqual: [UIColor redColor]]){
NSLog(@"Right colored laptop was fetched");
} else {
NSLog(@"Could not find the laptop with the given color.");
}
}
else {
NSLog(@"Could not fetch the laptop with the given color.
Error = %@", error);
}
return YES;
}
Правообладателям!
Это произведение, предположительно, находится в статусе 'public domain'. Если это не так и размещение материала нарушает чьи-либо права, то сообщите нам об этом.