Страницы

Поиск по вопросам

Показаны сообщения с ярлыком coredata. Показать все сообщения
Показаны сообщения с ярлыком coredata. Показать все сообщения

среда, 5 февраля 2020 г.

Core Data vs FMDB->sqlite

#objective_c #ios #coredata #sqlite


Стартовые условия - есть уже работающие сервер (MSSQL->JSON) и клиент (JSON->CoreData)
под iOS, и пишется клиент под Android который будет локально работать на sqlite. Руководство
хочет уйти от привязки к JSON'у на iOS, которую сделал исполнитель, и перевести iOS-клиент
на работу с sqlite, чтобы сделать в будущем highload-backend с которым потенциально
смогут работать обе мобильные платформы (а не два бекенда для каждой платформы как
сейчас). Передо мной (нынешний исполнитель) дилемма - перепиливать CoreData на прием
из сети sqlite и ее засовывание в CoreData, или переписывать все для FMDB->sqlite.
Все склоняется к тому чтобы использовать то, что будет быстрее работать на клиенте
визуально. Причиной создания данного топика стало то что возможности попробовать нет,
так как написание приложения на FMDB с нуля займет непозволительно много времени, и
если оно не окупится по итогу то много времени будет потрачено зря. Прошу не разводить
холивар на тему кому что больше нравится а описать какие-то практические данные с реальных
проектов, околограничные проблемы обоих решений, свой личный опыт о том что из этих
вариантов работает быстрее или имеет какие-то преимущества.     


Ответы

Ответ 1



Я немного запутался в вашем вопросе, но все равно попробую ответить. Мне странно, что ваше руководство "хочет уйти от привязки к JSON'у на iOS" и я, честно говоря, впервые слышу, чтобы работающий клиент (JSON->CoreData) перекраивали под то, чтобы работать с sqlite (то есть даже вообще впервые слышу, чтобы sqlite служил транспортом вместо JSON особенно в целях унификации платформ). Возможно я не знаю каких-то особых подходов и тонкостей, но, кажется, JSON является абсолютным стандартом при передаче данных в сетевой схеме взаимодействия между клиентом-приложением и сервером-бэкэндом. Я ни разу не встречал, чтобы топовые библиотеки в мире Apple/iOS работали с чем-то кроме JSON. Достаточно назвать AFNetworking и RestKit. Сказав это, добавлю что такое решение вашего руководства выглядит вдвойне странным в виду того, что, повторю еще раз, если я правильно понял, они стремятся добиться максимальной универсальности решения. Исходя из сказанного выше, мне кажется, что не должно быть никаких сомнений, что ваш сервер должен отдавать JSON (ну и замечательно будет, если все по REST-конвенциям спроектировано будет или уже), и должны быть клиенты iOS и Android соответственно, которые будут этот JSON переносить уже к себе с учетом своих специфик. Если сказанное до сих пор верно, можно уже поставить немного другой вопрос, на который я могу ответить тоже лишь частично и только про Apple/iOS: "ваш опыт налаживания подобных схем" и его подвопросы: стоит ли пользоваться RestKit (так как это единственное full-stack решение подобных задач, насколько я знаю работает только с Core Data), и если нет, то пользоваться ли Core Data или отказаться от него и использовать sqlite? Мой личный опыт такой: я занимаюсь разработкой именно iOS-приложений около 11 месяцев, все это время я делаю приложение, которое можно, сказав грубо, назвать клоном FourSquare - говорю это для того, чтобы показать, что я достаточно плотно столкнулся именно с вопросами взаимодействия сервера, спроектированного в соответствии с REST и отдающего JSON, и клиента, которому нужно этот JSON разложить в Core Data. Так получилось, что о RestKit узнал далеко не сразу. Поэтому оказалось, что научение тому, как сделать все грамотно, то есть разобраться с многопоточностью, GCD, Core Data, всему тому, что RestKit делает где-то за кулисами прямо из коробки, заняло у меня очень много времени (где-то месяцев 5). Позже оказалось (когда я-таки взглянул внимательно на RestKit), что для того, чтобы полностью реализовать работающую схему JSON<->Core Data, мне пришлось сделать код, весьма напоминающий по структуре код RestKit. Он не столь идеально абстрагирован по сравнению с RestKit, но с поставленными задачами справляется, и чем дальше продвигается мой проект, он все больше начинает на него походить, хотя и с небольшой разницей в наших подходах (кстати, судя по профилю на Гитхабе, автор RestKit работает на TripAdvisor - то есть вот against what он разрабатывает RK). Про свои недоумения относительно RestKit я задавал здесь на ХК вопрос, на который, к сожалению, так и не получил хотя бы одного внятного ответа: RestKit - за и против?. То есть после года экспериментов я все еще колеблюсь по поводу того, чтобы полностью перейти на RestKit, так как уже сам почти научился решать эти задачи и немного в более близком лично мне ключе, чем это делает RestKit, но, повторю, времени и усилий это заняло достаточно много! Резюмируя эту часть: если вы хотите сэкономить время на склейке JSON в Core Data, используйте готовое решение, - постарайтесь перейти на RestKit, если готового самописного еще не стало так много, что миграция окажется очень сложной --- RestKit прекрасно справится с задачей по переводу JSON в Core Data и обратно. Некоторые задачи там решаются настолько легко, что это и впрямь выглядит как magic. Теперь отвечу совсем немного про "Core Data vs FMDB->sqlite". Я начинал изучать iOS строго по гайдам и там все время мелькало Core Data, поэтому я опять же не сразу узнал, что бывают просто разнообразные ORM и оболочки и key-value store, работающие с sqlite. Мой результат - это то, что я вполне нормально освоился с Core Data и НИ РАЗУ не встретил СЕРЬЕЗНЫХ ограничений, которые заставляли бы меня подумать о переходе к каким-то другим решениям, хотя за это время я узнал о существовании не только FMDB, но и, например, таких вот решений: NanoStore, YapDatabase, iActiveRecord (особенно близкое и понятное мне, так как я имеющ опыт работы с ActiveRecord в Ruby on Rails). На этом пока все. Возможно позже я дополню свой ответ, если придумаю, как сделать его лучше. ОБНОВЛЕНО ПОЗЖЕ Я могу себе представить только один возможный случай, когда работа с sqlite (без JSON вообще) может оказаться адекватной - это случай так называемой оффлайн-работы. Приведу простой пример: у вас есть приложение с картами (Яндекс карты) и вы в момент начала пользования приложением скачиваете разом NN мегабайтовую карту и с этого момента пользуетесь исключительно скачанной картой, так как карта обновляется очень редко и вы можете быть уверены, что скачанная карта будет служить вам верой и правдой, хм, адекватно отражать местность и завтра и послезавтра. То есть, если база, с которой будет работать ваше приложение, является более или менее статичной, вы можете при первой загрузке приложения (в момент начала использования приложения) просто разово передавать ее одним большим файлом (чуть ли не дампом базы) и просто импортировать её средствами местной на клиенте DB (не узнавал, возможно ли такое в Core Data или в SQLite, но очевидно, что подходы должны существовать). Таким образом вы на корню рубите вопросы синхронизации, JSON, весь этот парсинг, маппинг, асинхронность и много других слов и проблем. Единственное требование к специфике приложения - это, чтобы оно не должно было постоянно лазить за чем-то очень свежим и постоянно обновляющимся на ваш сервер, в сеть. Мне кажется, мое описание этого случая вполне ясно. ОБНОВЛЕНО ЕЩЕ ПОЗЖЕ Интересный топик на SO, открытый авторами вопроса и ответа на эту тему: Improve process of mirroring server database to a client database via JSON.

Ответ 2



Последний раз с CoreData (с нуля) работал довольно давно, но периодически приходилось поддерживать существующие проекты. С sqlite приходилось работать в паре проектов, но последние проекты были в основном тонкими клиентами. Все последующие мысли не являются истинной правдой, они основаны на мои знаниях, а в них есть много пробелов (я плохо знаю CoreData). CoreData это не аналог и не замена sqlite. Это два абсолютно разных инструмента, и у них разные области применения. Если нужно делать какие-то очень хитрые запросы (например вычислять географическое расстояние), то здесь будет удобнее sqlite. В остальных случаях будет достаточно CoreData'ы. CoreData - Контекст и невалидные сущности. Если в контекст попала невалидная сущность, и вы проморгали ее удаление оттуда то сохранить контекст уже не удастся. - CoreData - отдельный стек, не похожий на SQL с которым мне приходилось работать до этого. Причем стек довольно большой, а т.к. область применения относительна невелика, то зачастую очень тяжело найти решение какой-нибудь нетривиальной проблемы. + Работает "из коробки", при использовании сторонних оберток (MagicalRecord, ObjectiveRecord) работа с ней становится гораздо приятней. + Система миграций, не нужно писать raw sql (зачастую это излишне) SQLite - Приходится много писать относительно низкоуровневых вещей, либо пользоваться каким-то враппером, но даже с тем же FMDB приходится писать много, т.к. это всего лишь адаптация sqlite3 API для Objective-C. Как я указал в комментарии, пытался писать свою обертку, но задача далеко не тривиальная, вылазит много граблей/костылей - Приходится руками создавать таблицы, накатывать миграции (iActiveRecord частично решает эту проблему). + Это все же SQL и используется на многих платформах, а потому гораздо проще найти решение проблем/вопросов. + Полный контроль над взаимодействием с БД. Резюме В последующих проектах при выборе между SQLite и CoreData я в первую очередь буду опираться на требования, т.к. оба подхода имеют свои минусы и плюсы, и как правило если что-то легко в CoreData, то сложно в SQLite. И наоборот.

четверг, 23 января 2020 г.

Неявное поведение поля NSManagedObject'а

#objective_c #ios #coredata


Имею некий NSManagedObject-обьект названный, скажем Object. У него есть поле info
типа NSString. По логике программы я должен иметь возможность сохранить в это поле
пустое значение, то есть строку нулевой длины. При сохранении я так и поступаю. Но
когда я фетчу данный обьект из Core Dat'ы и логирую значение этого поля оно уже равно
(null). Если была сохранена строка ненулевой длины то она отдается корректно. Если
это ошибка то не понимаю на каком этапе я могу ее допустить. В целях теста присваиваю
значение полю, делаю save контексту и сразу тут же его фетчу. Получаю такой результат.
Чем может быть обусловлено такое поведение?    


Ответы

Ответ 1



Ах, как я люблю вопросы, в которых (без бутылки) без research effort'а сразу и не разобраться. Если серьёзно, то мои тесты не подтверждают то, что вы говорите: ниже я привожу два тестовых сценария (у меня они оба выполняются). User - это просто тестовая entity, которая имеет поле name типа string с включенной (это дефолт у Xcode) опцией "optional" (то есть оно необязательное, то есть может быть nil'ом), я полагаю, что у вас на вашем текстовом поле optional тоже включено. Так вот первый сценарий - это мы приравниваем name тестового пользователя к пустой строке @"", сохраняем контекст и фетчим его заново. И убеждаемся, что поле таки является пустой строкой. Второй сценарий - это мы заведомо приравниваем name к nil и закономерно убеждаемся после фетча, что name - это nil. Вывод из всего этого такой: похоже вы сохраняете не строку, а nil. Некоторое время назад я создал на Github специальный репозиторий для того, чтобы изучать разные тонкости устройства Cocoa. Для этого я просто прописываю подробные тесты для соответствующих аспектов поведения Core Data или NS-объектов, которые хочу изучить. Ваш случай я только что расположил здесь см. CoreData_Integration. Если будет желание освоить такой Test-Driven способ изучения тонкостей Cocoa, можно постучаться ко мне в скайп, и я объясню, как это работает, так как инструкции по эксплуатации проекта пока что нет. describe(@"Behavior of string-NSString fields", ^{ context(@"When a string field is set to empty string", ^{ it(@"should return its value as an empty string (not nil!)", ^{ User *user = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([User class]) inManagedObjectContext:managedObjectContext()]; user.name = @""; [managedObjectContext() save:nil]; NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"User"]; NSError *error; NSArray *fetchResult = [managedObjectContext() executeFetchRequest:fetchRequest error:&error]; if (error) { NSLog(@"Error %@", error); } user = (User *)[fetchResult lastObject]; [[user.name should] equal:@""]; }); }); context(@"When a string field is set to nil", ^{ it(@"should return its value as nil", ^{ User *user = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([User class]) inManagedObjectContext:managedObjectContext()]; user.name = nil; [managedObjectContext() save:nil]; NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"User"]; NSError *error; NSArray *fetchResult = [managedObjectContext() executeFetchRequest:fetchRequest error:&error]; if (error) { NSLog(@"Error %@", error); } user = (User *)[fetchResult lastObject]; [[user.name should] beNil]; }); }); });

вторник, 7 января 2020 г.

IOS высота cell в зависимости от текста

#ios7 #ios #coredata #tableview


Я получаю обьект из CoreData следующим образом: 
self.name = [[abc anyObject] valueForKey:@"name"]

и отображаю его:
cell.nameLabel.text = [NSString stringWithFormat:@"%@",self.name];
cell.nameLabel.numberOfLines = 0;
cell.nameLabel.lineBreakMode = NSLineBreakByWordWrapping;

Помещается текст не полностью. Как менять высоту cell в зависимости от текста?    


Ответы

Ответ 1



- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { CGSize constraintSize = CGSizeMake(286.0f, CGFLOAT_MAX); UIFont *theFont = [UIFont systemFontOfSize:14.0f]; CGSize theSize; if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0) { CGRect frame = [[self.mArray objectAtIndex:indexPath.row] boundingRectWithSize:constraintSize options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading) attributes:@{NSFontAttributeName:theFont} context:nil]; theSize = frame.size; } else { theSize = [[self.mArray objectAtIndex:indexPath.row] sizeWithFont:theFont constrainedToSize:constraintSize lineBreakMode:NSLineBreakByWordWrapping]; } return theSize.height; } Обновлено для iOS 7 и выше.

Ответ 2



Наилучший общий способ, который я пока придумал, такой. Выносите весь контент (view'шки) из cell'а в отдельную view (можно её назвать как-то вроде BlaBlaCellContentView). В этой view заводите класс-метод, который вам возвратит высоту ячейки с определенными данными (типа + (CGFloat) heightForData:(Data *)data maxWidth:(CGFloat)maxWidth). В этом методе для вычисления высоты, просто делаете layout всех ваших вьюшек (как обычно делаете в layoutSubviews) и выставляете фрейм для самой вьюшки. Далее, в cell'е заводите weak синглтон (хотя необязательно weak, можно обычный синглтон) для этой вьюшки и делаете аналогичный метод для вычисления высоты, примерно такой: - (void) sizeToFitWithWidth:(CGFloat)width { self.Width = width; [self layout]; self.Height = self.bottomLabel.BottomY + kMarginY; } + (CGFloat) heightForProduct:(IHerbProduct *)product width:(CGFloat)width { CartItemCellContentView *sharedCellContentView = [self sharedWeakInstance]; sharedCellContentView.product = product; [sharedCellContentView sizeToFitWithWidth:width]; CGFloat height = sharedCellContentView.Height; return height; } (замечу, что .Width, .Height, BottomY - это из моей собственной категории для UIView, но из названий и так понятно, что они возвращают). Ну и код для weak синглтона (смысл - живет до тех пор, пока на него есть хотя бы одна ссылка. Если уничтожился, то при следующем вызове создастся заново): + (instancetype) sharedWeakInstance { static __weak id weakInstance = nil; id strongInstance = weakInstance; @synchronized(self) { if (!strongInstance) { strongInstance = [self new]; weakInstance = strongInstance; } } return strongInstance; } P.S. Пришёл к таком подходу, т.к.: sizeWithFont:constrainedToSize: не всегда даёт те же результаты, что и [UILabel sizeToFit]. Для сложных cell'ов приходится писать слишком много сложного кода для вычисления высоты - оказалось проще переиспользовать код для layout'а.

Ответ 3



Если у вас будет много ячеек разной высота, то можно вынести подсчет высоты в отдельный метод. - (CGFloat)textViewHeightForAttributedText: (NSAttributedString*)text andWidth: (CGFloat)width { UITextView *calculationView = [[UITextView alloc] init]; [calculationView setAttributedText:text]; CGSize size = [calculationView sizeThatFits:CGSizeMake(width, FLT_MAX)]; return size.height; } И потом уже получать высоту: UIFont *font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:16.0]; NSDictionary *attrsDictionary = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName]; NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:text attributes:attrsDictionary]; CGFloat height = [self textViewHeightForAttributedText:attrString andWidth:280];

Ответ 4



-(void)viewDidLoad { self.yourTableView.rowHeight = UITableViewAutomaticDimension; self.yourTableView.estimatedRowHeight = 44.0f; } Всё! :)

понедельник, 23 декабря 2019 г.

Парсинг чисел в JSON для сохранения в Core Data

#json #ios #coredata #objective_c #afnetworking


Я расширяю процедуру уже написанного предварительно дампа базы данных на iOS-клиент.
Клиент получает с сервера JSON наполненный значениями. Правильно ли для значений типа
"число" создавать в  Core Data-сущностях атрибут типа NSNumber? В предыдущей реализации
программисты решили передавать все значения (в том числе числовые) типом "строка" и
уже на клиенте разбирать что число а что стринга. Есть ли здесь какой-то подтекст (может
я могу столкнуться с какими-то неудобствами в будущем) или это просто неверная реализация?     


Ответы

Ответ 1



Ответ на первый вопрос: да, правильно - используйте NSNumber. Без вариантов. Ответ на второй вопрос: Желательно, чтобы ваш сервер по API отдавал числа всё-таки в числовом виде ("числа - это числа"), но если есть проблемы с тем, чтобы переделать серверную сторону, вам нужно будет просто "строковые" числа преобразовывать в NSNumber, используя NSNumberFormatter (первый гугл показывает How to convert NSString into NSNumber) Подтекст очень простой - вы тратите дополнительное время на преобразование чисел в строки (на сервере) и строк в числа (на iOS-клиенте). Это немного, но всё же маленький overhead. И вы тратите у себя на клиенте время на написание дополнительных NSNumberFormatter-деклараций. Резюме: если можете, заставьте парней на сервере это всё переделать. Будет чисто и правильно. И небольшое добавление: бывают такие люди, которые очень хорошо знают всякие HTTP-стандарты и REST-стандарты и всякие, в общем, стандарты. Так вот такие люди ответили бы на ваш вопрос точно также, как я, только ещё привели бы штук 10 сокрушительных доводов на 10 абзацев против того, чтобы использовались строки для передачи всего с API-сервера и ещё столько же в пользу передачи чисел. Публикую это добавление в качестве шутки.

Ответ 2



"1" и 1 это не одно и тоже. С другой стороны если вы получаете с сервера словарь или NSArray тип может быть выжен. Используйте для парсинга NSJSONSerialization и если данные с сервера будут корректны проблем быть не должно.

воскресенье, 15 декабря 2019 г.

Поиск по фетчу Core Dat'ы с использованием fetchedResultsController

#objective_c #ios #coredata


В наличии UITableView, контент которой заполняется из Core Data при помощи fetchedResultsController'а:
- (NSFetchedResultsController *) fetchedResultsController
{
    NSLog(@"fetchedResultsController");
    if (fetchedResultsController != nil)
    {
        return fetchedResultsController;
    }

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Client"
                                              inManagedObjectContext:[[PTDataManager
sharedManager] managedObjectContext]];
    [fetchRequest setEntity:entity];

    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"agency_server_id
== %@", agency.server_id];
    fetchRequest.predicate = predicate;

    NSSortDescriptor *sortByName1Descriptor = [[NSSortDescriptor alloc] initWithKey:@"lastname"
ascending:YES];
    NSSortDescriptor *sortByName2Descriptor = [[NSSortDescriptor alloc] initWithKey:@"firstname"
ascending:YES];
    NSSortDescriptor *sortByName3Descriptor = [[NSSortDescriptor alloc] initWithKey:@"middlename"
ascending:YES];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects: sortByName1Descriptor,
sortByName2Descriptor, sortByName3Descriptor, nil];

    fetchRequest.sortDescriptors = sortDescriptors;

    fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:fetchRequest
managedObjectContext:[[PTDataManager sharedManager] managedObjectContext] sectionNameKeyPath:nil
cacheName:nil];
    fetchedResultsController.delegate = self;
    return fetchedResultsController;
}

другие делегатские методы не реализованы так как таблица заполняется один раз, при
входе во viewController и больше не меняется. Стоит задача реализовать поиск по контенту
таблицы с отображением в таблице только результатов соответствующих значениям введенным
в searchBar. Реализовываю метод:
   - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
    [self reloadTableView];
}

и соответственно триггеруемый метод для отображения в таблице:
- (void)reloadTableView
{
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Client"
                                              inManagedObjectContext:[[PTDataManager
sharedManager] managedObjectContext]];
    [fetchRequest setEntity:entity];

    NSPredicate *filterPredicate;
    NSString *searchString = self.searchBar.text;
    NSMutableArray *predicateArray = [[NSMutableArray alloc] init];
    [predicateArray addObject:[NSPredicate predicateWithFormat:@"agency_server_id
CONTAINS[cd] %@", agency.server_id]];
    [predicateArray addObject:[NSPredicate predicateWithFormat:@"firstname CONTAINS[cd]
%@", searchString]];
    [predicateArray addObject:[NSPredicate predicateWithFormat:@"lastname CONTAINS[cd]
%@", searchString]];
    [predicateArray addObject:[NSPredicate predicateWithFormat:@"middlename CONTAINS[cd]
%@", searchString]];
    filterPredicate = [NSCompoundPredicate orPredicateWithSubpredicates:predicateArray];
    [fetchRequest setPredicate:filterPredicate];

    NSSortDescriptor *sortByName1Descriptor = [[NSSortDescriptor alloc] initWithKey:@"lastname"
ascending:YES];
    NSSortDescriptor *sortByName2Descriptor = [[NSSortDescriptor alloc] initWithKey:@"firstname"
ascending:YES];
    NSSortDescriptor *sortByName3Descriptor = [[NSSortDescriptor alloc] initWithKey:@"middlename"
ascending:YES];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects: sortByName1Descriptor,
sortByName2Descriptor, sortByName3Descriptor, nil];

    fetchRequest.sortDescriptors = sortDescriptors;
    fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:fetchRequest
managedObjectContext:[[PTDataManager sharedManager] managedObjectContext] sectionNameKeyPath:nil
cacheName:nil];
    fetchedResultsController.delegate = self;

    NSError *error = nil;
    if (![self.fetchedResultsController performFetch:&error])
    {
        NSLog(@"Unresolved error %@, %@", [error localizedDescription], [error localizedFailureReason]);
        abort();
    };

    [self.clientsTableView reloadData];
}

который похож на fetchedResultsController но как видно имеется отличия в ограничении
выборки. Проблема в том, что в таком виде вызов -reloadTableView дергает и сам fetchedResultsController,
в результате фетч меняется в процессе ввода символов в searchBar. В таблице появляется
все что вытянул fetchedResultsController и плюс все остальное что совпадает с содержимым
searchBarа. Визуально: при вводе символов в поле поиска таблица не сокращает количество
результатов а увеличивает.
Вопрос:
на каком этапе реализации поиска по уже фетченой выборке я делаю ошибку? Могу ли
я как-то изменить вторичный фетч для поиска по уже полученным из первичного фетча результатам?    


Ответы

Ответ 1



Вопрос решился через составной предикат с использованием +orPredicateWithSubpredicates: и +andPredicateWithSubpredicates:: NSPredicate *orPredicate = [NSCompoundPredicate orPredicateWithSubpredicates:[NSArray arrayWithObjects:firstPredicate, secondPredicate, nil]]; NSPredicate *andPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:thirdPredicate, fourthPredicate, nil]]; NSPredicate *finalPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:orPredicate, andPredicate, nil]]; [fetchRequest setPredicate:finalPredicate]; В таком виде через AND сохраняется результат предварительного фетча и через дополнительные OR происходит его фильтрация согласно критериям поиска

Ответ 2



Посмотрите первый ответ, похоже ваша ситуация: How to filter NSFetchedResultsController (CoreData) with UISearchDisplayController/UISearchBar

пятница, 5 июля 2019 г.

Копирование NSManagedObject-подкласса

Существует некий NSManagedObject-подкласс Visit. Появилась необходимость получить почти точную копию обьекта - из двадцати полей нужно изменить значение у трех. Можно ли сделать копию не таким трудоемким процессом как .... for(Visit *copiedVisit in arrayOfVisits) // массив обьектов с которых делаются копии { Visit *newVisit = (Visit *)[NSEntityDescription insertNewObjectForEntityForName:@"Visit" inManagedObjectContext:[self managedObjectContext]]; newVisit.date = [NSDate someNewDate]; // поле которое нужно изменить newVisit.name = copiedVisit.name; // далее переприсвоение значений полей одного объекта = полям другого newVisit.attribute = copiedVisit.attribute; newVisit.time = copiedVisit.time; ...
NSError *err = nil; if (![self managedObjectContext] save:&err]) { ... }; } а потом переприсвоением значений полей одного объекта полям другого? Некий аналог copy для NSManagedObject-подкласса?


Ответ

Насколько мне известно, у Core Data нет такой возможности из коробки. Мне кажется, вполне уместно определить для вашего класса Visit соответствующий метод -copyWithZone: в котором можно описать всю процедуру порождения копии, и затем пользоваться просто обычным copy, как вы и предполагаете в своем вопросе - только не "аналогом copy", а самым настоящим copy, потому что именно так он и работает - через определение copyWithZone.
Смотри мой ответ про клонирование UIView: Objective C: как клонировать UIView?

Core Data не сохраняется число

Здравствуйте! Создал простенькую программку с поддержкой Core Data, типа Document–Based app. Имеется текстовое поле, соединённое с arrayController'ом, который в свою очередь соединён с Entity, у которой есть атрибут "name" типа "string" и атрибут "score" типа "decimal". Введённая в текстовое поле строка сохраняется без проблем. Также имеется другое текстовое поле: в него вводится число и после нажатия кнопки "Go" значение из поля передаётся в метку. Метка соединена абсолютно точно так же как и первое текстовое поле, но почему-то значение метки не сохраняется. С чем это связано и как это исправить? Для наглядности, проект можно скачать отсюда: https://www.dropbox.com/sh/kfp61xu3i4zj3kz/QCnPtlVqtE


Ответ

У вас проблема в том, что "запись" в базу не происходит. Изыскания показали, что данные из TextField записываются в базу только после переключения "фокуса" из этой TextField в другое место. А у вас получается, что фокусировки нет совсем. Попробуйте дополнить код "принудительно" записю данных в Базу. Например: - (IBAction)changeScore:(id)sender { [_scoreLabel setDoubleValue:[_scoreField doubleValue]]; id saveObj = [_arrayController selectedObjects][0]; double newDob = [_scoreField doubleValue]; NSNumber * decim = [NSNumber numberWithDouble:newDob]; [saveObj setValue:decim forKey:@"score"]; }

воскресенье, 7 апреля 2019 г.

Core Data vs FMDB->sqlite

Стартовые условия - есть уже работающие сервер (MSSQL->JSON) и клиент (JSON->CoreData) под iOS, и пишется клиент под Android который будет локально работать на sqlite. Руководство хочет уйти от привязки к JSON'у на iOS, которую сделал исполнитель, и перевести iOS-клиент на работу с sqlite, чтобы сделать в будущем highload-backend с которым потенциально смогут работать обе мобильные платформы (а не два бекенда для каждой платформы как сейчас). Передо мной (нынешний исполнитель) дилемма - перепиливать CoreData на прием из сети sqlite и ее засовывание в CoreData, или переписывать все для FMDB->sqlite. Все склоняется к тому чтобы использовать то, что будет быстрее работать на клиенте визуально. Причиной создания данного топика стало то что возможности попробовать нет, так как написание приложения на FMDB с нуля займет непозволительно много времени, и если оно не окупится по итогу то много времени будет потрачено зря. Прошу не разводить холивар на тему кому что больше нравится а описать какие-то практические данные с реальных проектов, околограничные проблемы обоих решений, свой личный опыт о том что из этих вариантов работает быстрее или имеет какие-то преимущества.


Ответ

Я немного запутался в вашем вопросе, но все равно попробую ответить.
Мне странно, что ваше руководство "хочет уйти от привязки к JSON'у на iOS" и я, честно говоря, впервые слышу, чтобы работающий клиент (JSON->CoreData) перекраивали под то, чтобы работать с sqlite (то есть даже вообще впервые слышу, чтобы sqlite служил транспортом вместо JSON особенно в целях унификации платформ).
Возможно я не знаю каких-то особых подходов и тонкостей, но, кажется, JSON является абсолютным стандартом при передаче данных в сетевой схеме взаимодействия между клиентом-приложением и сервером-бэкэндом. Я ни разу не встречал, чтобы топовые библиотеки в мире Apple/iOS работали с чем-то кроме JSON. Достаточно назвать AFNetworking и RestKit. Сказав это, добавлю что такое решение вашего руководства выглядит вдвойне странным в виду того, что, повторю еще раз, если я правильно понял, они стремятся добиться максимальной универсальности решения.
Исходя из сказанного выше, мне кажется, что не должно быть никаких сомнений, что ваш сервер должен отдавать JSON (ну и замечательно будет, если все по REST-конвенциям спроектировано будет или уже), и должны быть клиенты iOS и Android соответственно, которые будут этот JSON переносить уже к себе с учетом своих специфик. Если сказанное до сих пор верно, можно уже поставить немного другой вопрос, на который я могу ответить тоже лишь частично и только про Apple/iOS: "ваш опыт налаживания подобных схем" и его подвопросы: стоит ли пользоваться RestKit (так как это единственное full-stack решение подобных задач, насколько я знаю работает только с Core Data), и если нет, то пользоваться ли Core Data или отказаться от него и использовать sqlite?
Мой личный опыт такой: я занимаюсь разработкой именно iOS-приложений около 11 месяцев, все это время я делаю приложение, которое можно, сказав грубо, назвать клоном FourSquare - говорю это для того, чтобы показать, что я достаточно плотно столкнулся именно с вопросами взаимодействия сервера, спроектированного в соответствии с REST и отдающего JSON, и клиента, которому нужно этот JSON разложить в Core Data.
Так получилось, что о RestKit узнал далеко не сразу. Поэтому оказалось, что научение тому, как сделать все грамотно, то есть разобраться с многопоточностью, GCD, Core Data, всему тому, что RestKit делает где-то за кулисами прямо из коробки, заняло у меня очень много времени (где-то месяцев 5). Позже оказалось (когда я-таки взглянул внимательно на RestKit), что для того, чтобы полностью реализовать работающую схему JSON<->Core Data, мне пришлось сделать код, весьма напоминающий по структуре код RestKit. Он не столь идеально абстрагирован по сравнению с RestKit, но с поставленными задачами справляется, и чем дальше продвигается мой проект, он все больше начинает на него походить, хотя и с небольшой разницей в наших подходах (кстати, судя по профилю на Гитхабе, автор RestKit работает на TripAdvisor - то есть вот against what он разрабатывает RK). Про свои недоумения относительно RestKit я задавал здесь на ХК вопрос, на который, к сожалению, так и не получил хотя бы одного внятного ответа: RestKit - за и против?. То есть после года экспериментов я все еще колеблюсь по поводу того, чтобы полностью перейти на RestKit, так как уже сам почти научился решать эти задачи и немного в более близком лично мне ключе, чем это делает RestKit, но, повторю, времени и усилий это заняло достаточно много!
Резюмируя эту часть: если вы хотите сэкономить время на склейке JSON в Core Data, используйте готовое решение, - постарайтесь перейти на RestKit, если готового самописного еще не стало так много, что миграция окажется очень сложной --- RestKit прекрасно справится с задачей по переводу JSON в Core Data и обратно. Некоторые задачи там решаются настолько легко, что это и впрямь выглядит как magic.
Теперь отвечу совсем немного про "Core Data vs FMDB->sqlite". Я начинал изучать iOS строго по гайдам и там все время мелькало Core Data, поэтому я опять же не сразу узнал, что бывают просто разнообразные ORM и оболочки и key-value store, работающие с sqlite.
Мой результат - это то, что я вполне нормально освоился с Core Data и НИ РАЗУ не встретил СЕРЬЕЗНЫХ ограничений, которые заставляли бы меня подумать о переходе к каким-то другим решениям, хотя за это время я узнал о существовании не только FMDB, но и, например, таких вот решений: NanoStore, YapDatabase, iActiveRecord (особенно близкое и понятное мне, так как я имеющ опыт работы с ActiveRecord в Ruby on Rails).
На этом пока все. Возможно позже я дополню свой ответ, если придумаю, как сделать его лучше.
ОБНОВЛЕНО ПОЗЖЕ
Я могу себе представить только один возможный случай, когда работа с sqlite (без JSON вообще) может оказаться адекватной - это случай так называемой оффлайн-работы. Приведу простой пример: у вас есть приложение с картами (Яндекс карты) и вы в момент начала пользования приложением скачиваете разом NN мегабайтовую карту и с этого момента пользуетесь исключительно скачанной картой, так как карта обновляется очень редко и вы можете быть уверены, что скачанная карта будет служить вам верой и правдой, хм, адекватно отражать местность и завтра и послезавтра. То есть, если база, с которой будет работать ваше приложение, является более или менее статичной, вы можете при первой загрузке приложения (в момент начала использования приложения) просто разово передавать ее одним большим файлом (чуть ли не дампом базы) и просто импортировать её средствами местной на клиенте DB (не узнавал, возможно ли такое в Core Data или в SQLite, но очевидно, что подходы должны существовать). Таким образом вы на корню рубите вопросы синхронизации, JSON, весь этот парсинг, маппинг, асинхронность и много других слов и проблем. Единственное требование к специфике приложения - это, чтобы оно не должно было постоянно лазить за чем-то очень свежим и постоянно обновляющимся на ваш сервер, в сеть. Мне кажется, мое описание этого случая вполне ясно.
ОБНОВЛЕНО ЕЩЕ ПОЗЖЕ
Интересный топик на SO, открытый авторами вопроса и ответа на эту тему: Improve process of mirroring server database to a client database via JSON

суббота, 16 марта 2019 г.

Неявное поведение поля NSManagedObject'а

Имею некий NSManagedObject-обьект названный, скажем Object. У него есть поле info типа NSString. По логике программы я должен иметь возможность сохранить в это поле пустое значение, то есть строку нулевой длины. При сохранении я так и поступаю. Но когда я фетчу данный обьект из Core Dat'ы и логирую значение этого поля оно уже равно (null). Если была сохранена строка ненулевой длины то она отдается корректно. Если это ошибка то не понимаю на каком этапе я могу ее допустить. В целях теста присваиваю значение полю, делаю save контексту и сразу тут же его фетчу. Получаю такой результат. Чем может быть обусловлено такое поведение?


Ответ

Ах, как я люблю вопросы, в которых (без бутылки) без research effort'а сразу и не разобраться. Если серьёзно, то мои тесты не подтверждают то, что вы говорите: ниже я привожу два тестовых сценария (у меня они оба выполняются). User - это просто тестовая entity, которая имеет поле name типа string с включенной (это дефолт у Xcode) опцией "optional" (то есть оно необязательное, то есть может быть nil'ом), я полагаю, что у вас на вашем текстовом поле optional тоже включено. Так вот первый сценарий - это мы приравниваем name тестового пользователя к пустой строке @"", сохраняем контекст и фетчим его заново. И убеждаемся, что поле таки является пустой строкой. Второй сценарий - это мы заведомо приравниваем name к nil и закономерно убеждаемся после фетча, что name - это nil Вывод из всего этого такой: похоже вы сохраняете не строку, а nil. Некоторое время назад я создал на Github специальный репозиторий для того, чтобы изучать разные тонкости устройства Cocoa. Для этого я просто прописываю подробные тесты для соответствующих аспектов поведения Core Data или NS-объектов, которые хочу изучить. Ваш случай я только что расположил здесь см. CoreData_Integration. Если будет желание освоить такой Test-Driven способ изучения тонкостей Cocoa, можно постучаться ко мне в скайп, и я объясню, как это работает, так как инструкции по эксплуатации проекта пока что нет. describe(@"Behavior of string-NSString fields", ^{ context(@"When a string field is set to empty string", ^{ it(@"should return its value as an empty string (not nil!)", ^{ User *user = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([User class]) inManagedObjectContext:managedObjectContext()];
user.name = @"";
[managedObjectContext() save:nil];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"User"];
NSError *error; NSArray *fetchResult = [managedObjectContext() executeFetchRequest:fetchRequest error:&error];
if (error) { NSLog(@"Error %@", error); }
user = (User *)[fetchResult lastObject];
[[user.name should] equal:@""]; }); });
context(@"When a string field is set to nil", ^{ it(@"should return its value as nil", ^{ User *user = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([User class]) inManagedObjectContext:managedObjectContext()];
user.name = nil;
[managedObjectContext() save:nil];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"User"];
NSError *error; NSArray *fetchResult = [managedObjectContext() executeFetchRequest:fetchRequest error:&error];
if (error) { NSLog(@"Error %@", error); }
user = (User *)[fetchResult lastObject];
[[user.name should] beNil]; }); }); });

понедельник, 19 ноября 2018 г.

Парсинг чисел в JSON для сохранения в Core Data

Я расширяю процедуру уже написанного предварительно дампа базы данных на iOS-клиент. Клиент получает с сервера JSON наполненный значениями. Правильно ли для значений типа "число" создавать в Core Data-сущностях атрибут типа NSNumber? В предыдущей реализации программисты решили передавать все значения (в том числе числовые) типом "строка" и уже на клиенте разбирать что число а что стринга. Есть ли здесь какой-то подтекст (может я могу столкнуться с какими-то неудобствами в будущем) или это просто неверная реализация?


Ответ

Ответ на первый вопрос: да, правильно - используйте NSNumber. Без вариантов.
Ответ на второй вопрос: Желательно, чтобы ваш сервер по API отдавал числа всё-таки в числовом виде ("числа - это числа"), но если есть проблемы с тем, чтобы переделать серверную сторону, вам нужно будет просто "строковые" числа преобразовывать в NSNumber, используя NSNumberFormatter (первый гугл показывает How to convert NSString into NSNumber)
Подтекст очень простой - вы тратите дополнительное время на преобразование чисел в строки (на сервере) и строк в числа (на iOS-клиенте). Это немного, но всё же маленький overhead.
И вы тратите у себя на клиенте время на написание дополнительных NSNumberFormatter-деклараций.
Резюме: если можете, заставьте парней на сервере это всё переделать. Будет чисто и правильно.
И небольшое добавление: бывают такие люди, которые очень хорошо знают всякие HTTP-стандарты и REST-стандарты и всякие, в общем, стандарты. Так вот такие люди ответили бы на ваш вопрос точно также, как я, только ещё привели бы штук 10 сокрушительных доводов на 10 абзацев против того, чтобы использовались строки для передачи всего с API-сервера и ещё столько же в пользу передачи чисел. Публикую это добавление в качестве шутки.

суббота, 27 октября 2018 г.

Поиск по фетчу Core Dat'ы с использованием fetchedResultsController

В наличии UITableView, контент которой заполняется из Core Data при помощи fetchedResultsController'а: - (NSFetchedResultsController *) fetchedResultsController { NSLog(@"fetchedResultsController"); if (fetchedResultsController != nil) { return fetchedResultsController; }
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"Client" inManagedObjectContext:[[PTDataManager sharedManager] managedObjectContext]]; [fetchRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"agency_server_id == %@", agency.server_id]; fetchRequest.predicate = predicate;
NSSortDescriptor *sortByName1Descriptor = [[NSSortDescriptor alloc] initWithKey:@"lastname" ascending:YES]; NSSortDescriptor *sortByName2Descriptor = [[NSSortDescriptor alloc] initWithKey:@"firstname" ascending:YES]; NSSortDescriptor *sortByName3Descriptor = [[NSSortDescriptor alloc] initWithKey:@"middlename" ascending:YES]; NSArray *sortDescriptors = [[NSArray alloc] initWithObjects: sortByName1Descriptor, sortByName2Descriptor, sortByName3Descriptor, nil];
fetchRequest.sortDescriptors = sortDescriptors;
fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:fetchRequest managedObjectContext:[[PTDataManager sharedManager] managedObjectContext] sectionNameKeyPath:nil cacheName:nil]; fetchedResultsController.delegate = self; return fetchedResultsController; } другие делегатские методы не реализованы так как таблица заполняется один раз, при входе во viewController и больше не меняется. Стоит задача реализовать поиск по контенту таблицы с отображением в таблице только результатов соответствующих значениям введенным в searchBar. Реализовываю метод: - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { [self reloadTableView]; } и соответственно триггеруемый метод для отображения в таблице: - (void)reloadTableView { NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"Client" inManagedObjectContext:[[PTDataManager sharedManager] managedObjectContext]]; [fetchRequest setEntity:entity];
NSPredicate *filterPredicate; NSString *searchString = self.searchBar.text; NSMutableArray *predicateArray = [[NSMutableArray alloc] init]; [predicateArray addObject:[NSPredicate predicateWithFormat:@"agency_server_id CONTAINS[cd] %@", agency.server_id]]; [predicateArray addObject:[NSPredicate predicateWithFormat:@"firstname CONTAINS[cd] %@", searchString]]; [predicateArray addObject:[NSPredicate predicateWithFormat:@"lastname CONTAINS[cd] %@", searchString]]; [predicateArray addObject:[NSPredicate predicateWithFormat:@"middlename CONTAINS[cd] %@", searchString]]; filterPredicate = [NSCompoundPredicate orPredicateWithSubpredicates:predicateArray]; [fetchRequest setPredicate:filterPredicate];
NSSortDescriptor *sortByName1Descriptor = [[NSSortDescriptor alloc] initWithKey:@"lastname" ascending:YES]; NSSortDescriptor *sortByName2Descriptor = [[NSSortDescriptor alloc] initWithKey:@"firstname" ascending:YES]; NSSortDescriptor *sortByName3Descriptor = [[NSSortDescriptor alloc] initWithKey:@"middlename" ascending:YES]; NSArray *sortDescriptors = [[NSArray alloc] initWithObjects: sortByName1Descriptor, sortByName2Descriptor, sortByName3Descriptor, nil];
fetchRequest.sortDescriptors = sortDescriptors; fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:fetchRequest managedObjectContext:[[PTDataManager sharedManager] managedObjectContext] sectionNameKeyPath:nil cacheName:nil]; fetchedResultsController.delegate = self;
NSError *error = nil; if (![self.fetchedResultsController performFetch:&error]) { NSLog(@"Unresolved error %@, %@", [error localizedDescription], [error localizedFailureReason]); abort(); };
[self.clientsTableView reloadData]; } который похож на fetchedResultsController но как видно имеется отличия в ограничении выборки. Проблема в том, что в таком виде вызов -reloadTableView дергает и сам fetchedResultsController, в результате фетч меняется в процессе ввода символов в searchBar. В таблице появляется все что вытянул fetchedResultsController и плюс все остальное что совпадает с содержимым searchBarа. Визуально: при вводе символов в поле поиска таблица не сокращает количество результатов а увеличивает. Вопрос: на каком этапе реализации поиска по уже фетченой выборке я делаю ошибку? Могу ли я как-то изменить вторичный фетч для поиска по уже полученным из первичного фетча результатам?


Ответ

Вопрос решился через составной предикат с использованием +orPredicateWithSubpredicates: и +andPredicateWithSubpredicates: NSPredicate *orPredicate = [NSCompoundPredicate orPredicateWithSubpredicates:[NSArray arrayWithObjects:firstPredicate, secondPredicate, nil]];
NSPredicate *andPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:thirdPredicate, fourthPredicate, nil]];
NSPredicate *finalPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:orPredicate, andPredicate, nil]]; [fetchRequest setPredicate:finalPredicate]; В таком виде через AND сохраняется результат предварительного фетча и через дополнительные OR происходит его фильтрация согласно критериям поиска