воскресенье, 12 июня 2016 г.

41. CoreData Part 1 Basics

Первый урок по CoreData.

Создаем пустой проект и указываем галочку Use CoreData.

Для отслеживания (дебага) запросов к CoreData выбираем схему (левый верхний угол окна) -> edit Scheme -> Слева выбираем Run и переходим на вкладку Arguments.
Добавляем новый параметр (+ нажать) в блок Arguments Passed On Launch:
-com.apple.CoreData.SQLDebug 1

В проекте присутствует 3 новых объекта

Модель  NSManagedObjectModel *managedObjectModel  связана с файлом в котором хранятся описания всех сущностей и связей, которые хранятся в базе данных.

Координатор NSPersistentStoreCoordinator *persistentStoreCoordinator он осуществляет считывание и сохранение данных в файл базы данных. Он использует модель, чтобы знать какие сущности хранятся в файле базы данных.

Контекст  NSManagedObjectContext *managedObjectContext  это та среда которая используется в приложении для создания, сохранения объектов в базу данных. Приложение взаимодействует с базой данных через контекст.  Контекст связан с координатором,  который отвечает за непосредственный обмен данных с файлом базы данных.     

Для создания новой сущности/модели нужно зайти в файл модели с расширением .xcdatamodeld и нажать кнопку Add Entity
Назовем новую сущность, так же как класс TAStudent
Добавим атрибуты в сущность fistName, LastName  - типа String, dateBirth - типа Date, score -типа Double.

Для того, чтобы создать объект в кордате он должен быть типа NSManagedObject. 
Добавляем объект студент - сущьность типа TAStudent и инициализируем его с помощью KVC:


 NSManagedObject *student = [NSEntityDescription insertNewObjectForEntityForName:@"TAStudent" inManagedObjectContext:self.managedObjectContext]; 
 [student setValue:@"Vasya" forKey:@"firstName"];
    [student setValue:@"Pupkin" forKey:@"lastName"];
    [student setValue:@3.8 forKey:@"score"];
    [student setValue:[NSDate dateWithTimeIntervalSinceReferenceDate:-60*60*24*365*10] forKey:@"dateBirth"];
    NSError *error;
    if (![self.managedObjectContext save:&error]) {
        NSLog(@"Error: %@", [error localizedDescription]);

    }

Если приложение падает с ошибкой, то это происходит из-за того, что первоначально в базе не было сущности, а мы добавили ее позже, а файл уже создан. Из-за этого происходит конфликт.
Самый простой способ это удалить приложение Simulator -> Hardware -> Home.

Можно добавить строчку, чтобы при конфликте старый файл удалялся. В функцию - (NSPersistentStoreCoordinator *)persistentStoreCoordinator {  

После условия 

if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {

Добавляем 

[[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil];

Для того, что сделать запрос из базы данных:

    NSFetchRequest *request = [[NSFetchRequest alloc]init];
    
    NSEntityDescription *description  = [NSEntityDescription entityForName:@"TAStudent" inManagedObjectContext:_managedObjectContext];
    [request setEntity:description];
    NSError *requestError = nil;
    NSArray *arrayResult = [self.managedObjectContext executeFetchRequest:request error:&requestError];
    if (requestError) {
        NSLog(@"Request Error: %@", [requestError localizedDescription]);
    }
    else{
        NSLog(@"Result %@", arrayResult);

    }

Можно выбрать тип результата для запроса, например результат в виде дикшионари для удобного отображения:

[request setResultType:NSDictionaryResultType];

Можно выдать результат в виде количества - объектов NSCountResultType, если сами объекты не нужны.

Если результат в виде массива объектов NSManagedObject, то вывести их можно так

    for (NSManagedObject *object in arrayResult) {
        NSLog(@"%@ %@ = %@", [object valueForKey:@"firstName"], [object valueForKey:@"lastName"], [object valueForKey:@"score"]);
    }

Для удобства работы с сущностями мы можем создать специальный класс наследник NSManagedObject, для этого добавляем в проект файл, слева выбираем Core Data, справа NSManagedObject subclass, выбираем файл с моделью и нашу сущность на основе нее будет создан новый класс.
Теперь для вывода результата можно использовать этот класс:

    for (TAStudent *student in arrayResult) {
        NSLog(@"%@ %@ = %@", student.firstName, student.lastName, student.score);

    }

Добавим функцию для создания рандомного студента.
Из прошлых проектов можно взять массивы имен и фамилий студентов.

static NSString* firstNames[] = {
    @"Tran", @"Lenore", @"Bud", @"Fredda", @"Katrice",
    @"Clyde", @"Hildegard", @"Vernell", @"Nellie", @"Rupert",
    @"Billie", @"Tamica", @"Crystle", @"Kandi", @"Caridad",
    @"Vanetta", @"Taylor", @"Pinkie", @"Ben", @"Rosanna",
    @"Eufemia", @"Britteny", @"Ramon", @"Jacque", @"Telma",
    @"Colton", @"Monte", @"Pam", @"Tracy", @"Tresa",
    @"Willard", @"Mireille", @"Roma", @"Elise", @"Trang",
    @"Ty", @"Pierre", @"Floyd", @"Savanna", @"Arvilla",
    @"Whitney", @"Denver", @"Norbert", @"Meghan", @"Tandra",
    @"Jenise", @"Brent", @"Elenor", @"Sha", @"Jessie"
};

static NSString* lastNames[] = {
    
    @"Farrah", @"Laviolette", @"Heal", @"Sechrest", @"Roots",
    @"Homan", @"Starns", @"Oldham", @"Yocum", @"Mancia",
    @"Prill", @"Lush", @"Piedra", @"Castenada", @"Warnock",
    @"Vanderlinden", @"Simms", @"Gilroy", @"Brann", @"Bodden",
    @"Lenz", @"Gildersleeve", @"Wimbish", @"Bello", @"Beachy",
    @"Jurado", @"William", @"Beaupre", @"Dyal", @"Doiron",
    @"Plourde", @"Bator", @"Krause", @"Odriscoll", @"Corby",
    @"Waltman", @"Michaud", @"Kobayashi", @"Sherrick", @"Woolfolk",
    @"Holladay", @"Hornback", @"Moler", @"Bowles", @"Libbey",
    @"Spano", @"Folson", @"Arguelles", @"Burke", @"Rook"

};

- (TAStudent *)addRandomStudent{
    TAStudent *student = [NSEntityDescription insertNewObjectForEntityForName:@"TAStudent" inManagedObjectContext:_managedObjectContext];
    student.score = @(arc4random_uniform(201)/100.f +2.f);
    student.dateBirth = [NSDate dateWithTimeIntervalSince1970:arc4random_uniform(20)*60*60*24*365];
    student.lastName = lastNames[arc4random_uniform(50)];
    student.firstName = firstNames[arc4random_uniform(50)];
    return student;

}

Выполняем эту функцию и сохраняем контекст:

    [self addRandomStudent];
    [self.managedObjectContext save:nil];

Каждый объект имеет ссылку на контекст, поэтому если мы находимся в другом классе (где нет self.managedObjectContext можно вызвать сохранение контекста так:

    TAStudent *student = [self addRandomStudent];
    [student.managedObjectContext save:nil];

Для переопределения сеттера и геттера в классе наследнике NSManagedObject, необходимо сообщить обсерверам CoreData об изменении проперти для сеттера или про получения доступа для геттера:

- (void)setFirstName:(NSString *)firstName
{
    [self willChangeValueForKey:@"firstName"];
    [self setPrimitiveValue:firstName forKey:@"firstName"];
    [self didChangeValueForKey:@"firstName"];
    NSLog(@"SET FIRST NAME");
}

- (NSString*)firstName
{
    NSString *string = nil;
    [self willAccessValueForKey:@"firstName"];
    string = [self primitiveValueForKey:@"firstName"];
    [self didAccessValueForKey:@"firstName"];
    NSLog(@"Get FIRST NAME");
    return string;
}

Так же можно сделать валидацию значений для проперти, аналогично предыдущему уроку по KVC:

- (BOOL) validateValue:(id  _Nullable __autoreleasing *)value forKey:(NSString *)key error:(NSError * _Nullable __autoreleasing *)error
{
    if ([key isEqualToString:@"lastName"]) {
        *error = [NSError errorWithDomain:@"BAD LAST NAME" code:123 userInfo:nil];
        return NO;
    }
    
    return YES;
}

Можно определить так же ф-ю конкретно для определенного ключа вида:

- (BOOL) validateLastName:(id  _Nullable __autoreleasing *)value error:(NSError * _Nullable __autoreleasing *)error

Если функция валидации возвращает NO, то при сохранении контекста возникнет эта ошибка и объект не будет сохранен.

Исходный код урока.

Домашнее задание по всей CoreData будет в конце 44 урока. 

суббота, 11 июня 2016 г.

40. KVC - Key Value Coding. KVO - Key Value Observing.

Урок 40  KVC - Key Value Coding. KVO - Key Value Observing.
Вводный  урок перед CoreData. Программирование через ключ - значение. И наблюдатели за изменением ключа - значения.

Создаем пустой проект.
Добавляем класс Студент с именем и возрастом.
Переопределяем сеттеры и определяем метод Description:

@interface TAStudent : NSObject
@property (strong, nonatomic) NSString *nameStudent;
@property (assign, nonatomic) NSInteger ageStudent;

@end

@implementation TAStudent

- (void)setNameStudent:(NSString *)nameStudent
{
    _nameStudent = nameStudent;
    NSLog(@"setNameStudent: %@",nameStudent);
    
}

- (void)setAgeStudent:(NSInteger)ageStudent
{
    _ageStudent = ageStudent;
    NSLog(@"setAgeStudent: %@",@(ageStudent));
    
}

- (NSString *)description
{
    return [NSString stringWithFormat:@"Student Name: %@, Age %@", self.nameStudent, @(self.ageStudent)];
}

Создаем Студената и устанавливаем свойства обычным способом.

Можно поменять свойства с помощью KVC следующим способом

[student setValue:@"Denis" forKey:@"nameStudent"];

В этом случае так же сработает сеттер setNameStudent.

Вот такой вывод мы получаем после запуска программы:

setNameStudent: Alex
setAgeStudent: 20
Student Name: Alex, Age 20
setNameStudent: Denis
Student Name: Denis, Age 20

Чтобы использовать KVC для примитивов нужно исползовать NSNumber, для структур (CGPoint, SGSize) - NSValue:

    [student setValue:[NSNumber numberWithInteger:25] forKey:@"ageStudent"];

Можно считывать данные проперти с помощью KVC (valueForKey):

 NSLog(@"name1 = %@, name2 = %@", student.nameStudent, [student valueForKey:@"nameStudent"]);

Если неправильно написать ключ (несуществующий), то приложение упадет. Чтобы этого не произошло можно переопределить метод геттер valueForUndefinedKey и сеттер setValue: forUndefinedKey:

- (id)valueForUndefinedKey:(NSString *)key
{
    NSLog(@"valueForUndefinedKey %@", key);
    return @"Unknown";
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
    NSLog(@"setValue %@ forUndefinedKey %@", value, key);
    
}

Переходим к KVO. 
В KVO можно назначить наблюдателя на конкретное проперти любого объекта, который будет вызывать метод, когда это проперти будет меняться. Это очень удобно и интересно, т.к. можно обойтись без делегата.

Чтобы существовала активная ссылка на нашего студенат, заведем проперти:


@property (strong, nonatomic) TAStudent *student;

Определим метод который срабатывает при изменении проперти:


#pragma mark - Observing

- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    NSLog(@"observeValueForKeyPath. keyPath: %@. object: %@, chang: %@", keyPath, object, change);
    

}

Создадим наблюдателя за нужным полем. Наблюдатель будет наш контроллер:

    [self.student addObserver:self forKeyPath:@"nameStudent" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

Тут же отписываемся от наблюдателя в Dealloc:

- (void)dealloc
{
    [self.student removeObserver:self forKeyPath:@"nameStudent"];
}

Вот результат вывода программы:

 setNameStudent: Alex
 observeValueForKeyPath. keyPath: nameStudent. object: Student Name: Alex, Age 0, chang: {
    kind = 1;
    new = Alex;
    old = "<null>";
}
 setAgeStudent: 20
 Student Name: Alex, Age 20
 Student Set value Denis for key nameStudent
 setNameStudent: Denis
 observeValueForKeyPath. keyPath: nameStudent. object: Student Name: Denis, Age 20, chang: {
    kind = 1;
    new = Denis;
    old = Alex;
}
 Student Set value 25 for key ageStudent
 setAgeStudent: 25
 name1 = Denis, name2 = Denis
 Student Set value Vanya for key Key
 setValue Vanya forUndefinedKey Key

 Student Name: Denis, Age 25

Внутри метода  observeValueForKeyPath  можно взять новое значение из дикшионари:


id newValue = [change objectForKey:NSKeyValueChangeNewKey];

Если создать метод в студенте, который меняет проперти через iVar (_nameStudent =), то обсервер не вызовется, а если через self (self.nameStudent =),  то обсервер сработает. Нужно это иметь ввиду.


- (void)changeName
{
    NSLog(@"ChangeName to FakeName");
    _nameStudent = @"FakeName";
}

Для того, чтобы обсервер увидел изменение проперти по айвар, необходимо добавить 2 метода willChangeValueForKey до изменения и didChangeValueForKey после:

- (void)changeName
{
    NSLog(@"ChangeName to FakeName");
    [self willChangeValueForKey:@"nameStudent"];
    _nameStudent = @"FakeName";
    [self didChangeValueForKey:@"nameStudent"];
}


Следующая часть урока.
Создаем новый класс TAGroup в котором находится массив студентов:

@interface TAGroup : NSObject

@property (strong, nonatomic) NSArray *arrayStudents;


@end

Создаем студентов и группу,  добавляем студентов в массив группы:

    TAStudent *student = [[TAStudent alloc] init];
    student.nameStudent = @"Alex";
    student.ageStudent = 20;

    TAStudent *student1 = [[TAStudent alloc] init];
    student1.nameStudent = @"Stas";
    student1.ageStudent = 22;

    TAStudent *student2 = [[TAStudent alloc] init];
    student2.nameStudent = @"Ivan";
    student2.ageStudent = 21;

    TAStudent *student3 = [[TAStudent alloc] init];
    student3.nameStudent = @"Alexey";
    student3.ageStudent = 23;
    
    TAGroup *group1 = [[TAGroup alloc] init];

    group1.arrayStudents = @[student, student1, student2, student3];

С помощью KVC мы можем удалить из массива NSArray элемент, не превращая массив в NSMutableArray:

    NSMutableArray *arrayMutable = [group1 mutableArrayValueForKey:@"arrayStudents"];
    [arrayMutable removeLastObject];

Или в одну строчку

[[group1 mutableArrayValueForKey:@"arrayStudents"] removeLastObject];

Так же можно использовать путь до ключа valueForKeyPath

NSLog(@"KeyPath. name = %@",[self valueForKeyPath:@"student.nameStudent"]);

Для того чтобы проверить возможность ввода определенного значения в проперти используем validateValue:

    NSString *newName = @"Alex123";
    NSError *error = nil;
    if (![self.student validateValue:&newName forKey:@"nameStudent" error:&error]) {
        NSLog(@"Validate error: %@", error);

    }

В классе TAStudent переопределяем метод validateValue:

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError
{
    if ([inKey isEqualToString:@"nameStudent"]) {
        NSString *newName = *ioValue;
        if (![newName isKindOfClass:[NSString class]]) {
            *outError = [[NSError alloc] initWithDomain:@"Not Nsstring" code:123 userInfo:nil];
            return NO;
        }
        if ([newName rangeOfString:@"1"].location != NSNotFound) {
            *outError = [[NSError alloc] initWithDomain:@"Has numbers" code:1234 userInfo:nil];
            return NO;
        }
    }
    return YES;
}

При выполнение сработает проверка и выведет:

 Validate error: Error Domain=Has numbers Code=1234 "(null)"

 Можно проверку делать не в общем методе (и проверять ключ), а для каждого ключа описать свой метод. Для этого убираем общий метод validateValue (иначе частный метод не сработает) и определяем частный метод для проперти .nameStudent:


- (BOOL)validateNameStudent:(inout id  _Nullable __autoreleasing *)ioValue error:(out NSError * _Nullable __autoreleasing *)outError
{
    NSLog(@"validateNameStudent");
    return YES;

}

Следующий момент - вывод длинны массива.

Создадим дополнительных студентов и новую группу:

    TAStudent *student5 = [[TAStudent alloc] init];
    student5.nameStudent = @"Vasya";
    student5.ageStudent = 21;
    
    TAStudent *student6 = [[TAStudent alloc] init];
    student6.nameStudent = @"Sergey";
    student6.ageStudent = 23;
    
    TAGroup *group2 = [[TAGroup alloc] init];

    group2.arrayStudents = @[student5, student6];

Объявим новое проперти -массив групп:


@property (strong, nonatomic) NSArray *arrayGroup;

Добавим в массив наши группы. Чтобы вывести длинну массива используем оператор коллекций @count: 


    self.arrayGroup = @[group1, group2];


    NSLog(@"group count %@", [self valueForKeyPath:@"arrayGroup.@count"]);

Чтобы объединить массивы студентов во всех группах и исключить одинаковые объекты с помощью одной строки кода оператор коллекций @distinctUnionOfArrays

    NSArray *arrayAllStudents = [self valueForKeyPath:@"arrayGroup.@distinctUnionOfArrays.arrayStudents"];
    NSLog(@"all students = %@", arrayAllStudents);

Информация по всем ключам можно посмотреть в документации Apple к KVC -> Collection Operators. 



Для вывода минимального,  максимального, среднего возраста студентов и суммы всех возрастов:

    NSNumber *minAge = [arrayAllStudents valueForKeyPath:@"@min.ageStudent"];
    NSNumber *maxAge = [arrayAllStudents valueForKeyPath:@"@max.ageStudent"];
    NSNumber *sumAge = [arrayAllStudents valueForKeyPath:@"@sum.ageStudent"];
    NSNumber *avgAge = [arrayAllStudents valueForKeyPath:@"@avg.ageStudent"];
    
    NSLog(@"minAge = %@, maxAge = %@, sumAge = %@, avgAge = %@", minAge, maxAge, sumAge, avgAge);

Для объединения объектов - имен студентов в отдельный массив используем оператор коллекций @distinctUnionOfObjects:

    NSArray *arrayAllNames = [arrayAllStudents valueForKeyPath:@"@distinctUnionOfObjects.nameStudent"];
    NSLog(@"arrayAllNames: %@", arrayAllNames);
 

Исходный код к  уроку 40.



Домашнее задание  для урока 40 KVC, KVO.

Ученик.

1. Создайте класс студента с пропертисами firstName, lastName, dateOfBirth, gender, grade
2. Также создайте статическую таблицу куда все эти данные выводятся и где их можно менять (с текст филдами, сенгментед контролами и тд)
3. Пропертисы вашего студента меняйте тем же образом что и в предыдущих уроках (через делегаты и акшины)

Студент.

4. Повесте обсервера на все пропертисы студента и выводите в консоль каждый раз, когда проперти меняется 
5. Также сделайте метод "сброс", который сбрасывает все пропертисы, а в самом методе не используйте сеттеры, сделайте все через айвары, но сделайте так, чтобы обсервер узнал когда и что меняется. (типо как в уроке)

Мастер.

забудьте про UI

6. Создайте несколько студентов и положите их в массив, но обсервер оставьте только на одном из них
7. У студентов сделайте weak проперти "friend". Сделайте цепочку из нескольких студентов, чтобы один был друг второму, второй третьему, тот четвертому, а тот первому :)
8. Используя метод setValue: forKeyPath: начните с одного студента (не того, что с обсервером) и переходите на его друга, меняя ему проперти, потом из того же студента на друга его друга и тд (то есть путь для последнего студента получится очень длинный)
9. Убедитесь что на каком-то из друзей, когда меняется какой-то проперти, срабатывает ваш обсервер

Супермен

10. Добавьте побольше студентов
11. Используя операторы KVC создайте массив имен всех студентов
12. Определите саммый поздний и саммый ранний годы рождения
13. Определите сумму всех баллов студентов и средний бал всех студентов