Первое, что вы должны сделать в ООП, это рассмотреть классы объектов.Какао использует архитектуру MVC (модель, представление, контроллер), поэтому классы должны соответствовать одной из этих трех категорий.Какао уже предоставляет класс NSTableView, который работает довольно хорошо, поэтому он оставляет модель и контроллер.
Существует несколько различных подходов к классу модели, которые вы можете использовать:
- ВыМожно написать класс таблицы функций, который содержит значения x и y в отдельных массивах
- Вы можете написать класс таблицы функций, который имеет один массив пар (x, y).
- В этой или предыдущей реализации вы могли бы предоставить общедоступный интерфейс, который поддерживает оба устройства (т. Е. У них есть методы, которые возвращают ay при заданном x, и свойства, которые являются x, y и (x,у) коллекции).Некоторые детали реализации будут зависеть от того, как вы соединяете табличное представление с данными (связывания или более старый NSTableViewDataSource протокол ).
- Вы также можете использоватьмассив значений x и создайте преобразователь значений .При таком подходе значения y существуют в табличном представлении, а не в модели.
- И т. Д.
Требования к приложению будут определять, какой подход выбрать.Я покажу вам подход преобразования значений, так как он требует наименьшего количества кода.
Для контроллера вы можете положиться на NSArrayController (который довольно хорошо работает с NSTableView) или создать свой собственный.Например, вы можете использовать NSMutableArray в качестве модели и создать контроллер, который отображает значения из массива в другие значения.Этот контроллер может выполнять отображение, используя блоки или некоторые классы функций, которые вы определяете.
Как видите, вариантов довольно много.Я собираюсь пойти с опцией, которая требует наименьшего кодирования: преобразователь значения, NSArrayController для контроллера и NSMutableArray (хранится в объекте, который также хранит преобразователь значения) для модели.Далее код должен храниться в файлах в соответствии со стандартным соглашением: каждый интерфейс и реализация находятся в отдельном файле с именем, равным классу, и расширением «.h» для интерфейсов и «.m» для реализации.Я также не буду беспокоиться об общих операторах импорта, таких как Cocoa / Cocoa.h и собственный интерфейс реализации каждого класса.
Во-первых, преобразователь значения.На самом деле их два, абстрактный суперкласс и конкретный подкласс.Это разделение позволяет легко добавлять другие типы функций позже.Суперкласс FunctionTransformer
очень прост.Все, что нужно переопределить из своей базы, NSValueTransformer
, это метод, который возвращает класс преобразованных значений, transformedValueClass
:
@interface FunctionTransformer : NSValueTransformer
+ (Class)transformedValueClass;
@end
@implementation Function
+ (Class)transformedValueClass {
return [NSNumber class];
}
@end
Конкретный подкласс, LinearTransformer
, необходимо переопределить основной метод преобразователей значения: transformedValue:
.Поскольку линейные преобразования являются обратимыми, мы также предоставим reverseTransformedValue:
.Также потребуются свойства для значений наклона и перехвата.
#import "FunctionTransformer.h"
@interface LinearTransformer : FunctionTransformer {
NSNumber *m_;
NSNumber *b_;
}
@property (nonatomic,retain) NSNumber *slope;
@property (nonatomic,retain) NSNumber *intercept;
+ (BOOL)allowsReverseTransformation;
-(id)init;
-(id)initWithSlope:(float)slope;
-(id)initWithIntercept:(float)intercept;
-(id)initWithSlope:(float)slope intercept:(float)intercept;
-(void)dealloc;
-(NSNumber*)transformedValue:(id)value;
-(NSNumber*)reverseTransformedValue:(id)value;
@end
@implementation LinearTransformer
@synthesize slope=m_, intercept=b_;
+(BOOL)allowsReverseTransformation {
return YES;
}
-(id)initWithSlope:(float)m intercept:(float)b {
if ((self = [super init])) {
m_ = [[NSNumber alloc] initWithFloat:m];
b_ = [[NSNumber alloc] initWithFloat:b];
}
return self;
}
-(id)init {
return [self initWithSlope:1.0 intercept:0.0];
}
-(id)initWithSlope:(float)slope {
return [self initWithSlope:slope intercept:0.0];
}
-(id)initWithIntercept:(float)intercept {
return [self initWithSlope:1.0 intercept:intercept];
}
-(void)dealloc {
[b release];
[m release];
[super dealloc];
}
-(NSNumber*)transformedValue:(id)value {
return [NSNumber numberWithFloat:([value floatValue] * [m floatValue] + [b floatValue])];
}
-(NSNumber*)reverseTransformedValue:(id)value {
return [NSNumber numberWithFloat:(([value floatValue] - [b floatValue]) / [m floatValue])];
}
@end
Для использования необходимо зарегистрировать определенный LinearTransformer
, чтобы можно было установить наклон и перехват.Делегат приложения может владеть этим преобразователем (вместе с коллекцией значений x), или вы можете написать собственный контроллер.Мы собираемся написать класс модели, который объединяет значения x и преобразователь значений с именем FunctionTable
.Настройка функции преобразователя требует подзадач: зарегистрировать преобразователь в качестве преобразователя значения (используя +setValueTransformer:forName:
).Это означает, что нам нужно предоставить наш собственный установщик (setF:
) для свойства преобразователя функции (f
).
#import "FunctionTransformer.h"
extern NSString* const kFunctionTransformer;
@interface FunctionTable : NSObject {
NSMutableArray *xs;
FunctionTransformer *f;
}
@property (nonatomic,retain) IBOutlet NSMutableArray *xs;
@property (nonatomic,retain) IBOutlet FunctionTransformer *f;
@end
// FunctionTable.m:
#import "LinearTransformer.h"
NSString* const kFunctionTransformer = @"Function Transformer";
@implementation FunctionTable
@synthesize xs, f;
-(id) init {
if ((self = [super init])) {
xs = [[NSMutableArray alloc] init];
self.f = [[LinearTransformer alloc] init];
[f release];
}
return self;
}
-(void)dealloc {
[f release];
[xs release];
[super dealloc];
}
-(void)setF:(FunctionTransformer *)func {
if (func != f) {
[f release];
f = [func retain];
[NSValueTransformer setValueTransformer:f forName:kFunctionTransformer];
}
}
@end
По умолчанию FunctionTable
использует LinearTransformer
. Если вы хотите использовать другой, просто установите свойство FunctionTables
f
. Вы можете сделать это в Interface Builder (IB), используя привязки . Обратите внимание, что в этой упрощенной реализации преобразователь значения всегда регистрируется под именем «Function Transformer», фактически ограничивая вас одним FunctionTable
. Более сложной схемой было бы дать каждому FunctionTable
свое собственное имя преобразователя функции, которое будет использоваться при регистрации их собственного FunctionTransformer
.
Чтобы все настроить:
- Откройте перо главного окна приложения в IB.
- Создание экземпляра NSArrayController и FunctionTable (и вашего пользовательского делегата приложения, если есть).
- К главному окну добавить:
- Кнопки для добавления и удаления элементов,
- меток и NSTextFields для наклона и точки пересечения,
- NSTableView.
- Установите заголовки таблиц на "x" и "y" (необязательно для работы приложения)
- Настройка соединений :
- Попросите кнопки добавления и удаления отправить действия NSArrayController
add:
и remove:
.
- Привязать значения NSTextFields к путям клавиш
f.slope
и f.intercept
FunctionTables.
- Привязать значения обоих столбцов NSTableView к FunctionTables
xs
.
- Установить преобразователь значения для второго столбца в «Функциональный преобразователь»
- Привязать массив содержимого NSArrayController к клавише
xs
в FunctionTable.
- Если у вас есть делегат приложения, подключите его к розетке владельца файла
delegate
.
Теперь соберите и запустите. Вы можете использовать кнопки добавления и удаления для добавления и удаления строк в / из таблицы. Вы можете редактировать столбцы «x» и «y» подряд (последний благодаря reverseTransformedValue:
). Вы можете сортировать по столбцам «x» или «y». Вы можете изменить наклон и перехват, хотя вы не заметите обновления в таблице, если не выберете строки по отдельности.
Дополнительные темы
Чтобы исправить проблему обновления табличного представления, нам нужно распространить изменения свойств одного объекта (FunctionTransformer
) на изменения свойств другого (a FunctionTable
). У нас будет FunctionTable
наблюдать изменения свойств его функционального преобразователя и, когда он FunctionTable
получит уведомление об изменении любого такого свойства, отправит уведомление об изменении свойства xs
( что немного злоупотребляет, так как xs
фактически не изменилось). Это станет немного волшебным, так что терпите меня.
Объект подписывается на изменения другого объекта, используя KVO метод addObserver:forKeyPath:options:context:
другого объекта, и отменяет подписку, используя removeObserver:forKeyPath:
. Эти методы просто нужно вызывать, а не писать. Уведомления обрабатываются методом observeValueForKeyPath:ofObject:change:context:
объекта наблюдения, поэтому этот метод необходимо написать. Наконец, объект может отправлять свои собственные уведомления, вызывая willChangeValueForKey:
и didChangeValueForKey:
. Существуют и другие способы отправки уведомлений об изменении только части коллекции, но мы не будем их здесь использовать.
Наш FunctionTable
может обрабатывать изменения подписки и отмены подписки, но затем он должен знать, какие свойства функционального преобразователя следует соблюдать, что означает, что вы не можете изменить тип преобразователя. Вы можете добавить методы к каждому конкретному преобразователю функции, чтобы подписаться и отписаться от наблюдателя:
@implementation LinearTransformer
...
-(void)addObserver:(NSObject *)observer
options:(NSKeyValueObservingOptions)options
context:(void *)context
{
[self addObserver:observer
forKeyPath:@"slope"
options:options
context:context];
[self addObserver:observer
forKeyPath:@"intercept"
options:options
context:context];
}
-(void)removeObserver:(id)observer {
[self removeObserver:observer forKeyPath:@"slope"];
[self removeObserver:observer forKeyPath:@"intercept"];
}
@end
Однако это потребует значительного повторения кода в каждом методе и для каждого конкретного преобразователя функции.Использование некоторой магии ( отражение и замыкания или, как их называют в Objective-C, блоков ( [2] ))мы можем добавить методы (названные addObserver:options:context:
и removeObserver:
, так как они функционально аналогичны методам KVO для подписки и отписки) к FunctionTransformer
или даже к NSObject
.Поскольку наблюдение всех свойств объекта не ограничивается FunctionTransformer
s, мы добавим методы к NSObject
.Чтобы это работало, вам понадобятся либо OS X 10.6, либо PLBlocks и OS X 10.5.
Давайте начнем сверху вниз с изменениями FunctionTable
.Теперь появились новые подзадачи при настройке функции преобразователя: отмена подписки на изменения старого преобразователя и подписка на изменения нового.Таким образом, метод setF:
необходимо обновить, чтобы использовать новые методы NSObject
, которые будут определены в заголовке с именем "NSObject_Properties.h".Обратите внимание, что нам пока не нужно беспокоиться о реализации этих методов.Мы можем использовать их здесь, веря, что позже напишем подходящие реализации.FunctionTable
также нужен новый метод для обработки уведомлений об изменениях (observeValueForKeyPath:ofObject:change:context:
, о котором говорилось ранее).
#import "NSObject_Properties.h"
@interface FunctionTable
...
-(void)setF:(FunctionTransformer *)func {
if (func != f) {
[f removeObserver:self];
[f release];
f = [func retain];
[f addObserver:self
options:NSKeyValueObservingOptionPrior
context:NULL];
[NSValueTransformer setValueTransformer:f forName:kFunctionTransformer];
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if (object == f) {
if ([[change objectForKey:NSKeyValueChangeNotificationIsPriorKey] boolValue]) {
[self willChangeValueForKey:@"xs"];
} else {
[self didChangeValueForKey:@"xs"];
}
}
}
Далее мы пишем новые методы для NSObject
.Методы подписки или отмены подписки на изменения будут зацикливаться на свойствах объекта, поэтому нам понадобится вспомогательный метод forEachProperty
для выполнения цикла.Этот вспомогательный метод примет блок, который он вызывает для каждого свойства.Методы подписки и отмены подписки будут просто вызывать forEachProperty
, передавая блок, который вызывает стандартные методы KVO (addObserver:forKeyPath:options:context:
и removeObserver:forKeyPath:
) для каждого свойства для добавления или удаления подписок.
//NSObject_Properties.h
#import <Cocoa/Cocoa.h>
#import <objc/runtime.h>
@interface NSObject (Properties)
typedef void (^PropertyBlock)(objc_property_t prop, NSString *name);
-(void)forEachProperty:(PropertyBlock)block;
-(void)addObserver:(id)observer options:(NSKeyValueObservingOptions)options context:(void *)context;
-(void)removeObserver:(id)observer;
@end
// NSObject_Properties.m:
...
@implementation NSObject (Properties)
-(void)forEachProperty:(PropertyBlock)block {
unsigned int propCount, i;
objc_property_t * props = class_copyPropertyList([self class], &propCount);
NSString *name;
for (i=0; i < propCount; ++i) {
name = [[NSString alloc]
initWithCString:property_getName(props[i])
encoding:NSUTF8StringEncoding];
block(props[i], name);
[name release];
}
free(props);
}
-(void)addObserver:(NSObject *)observer
options:(NSKeyValueObservingOptions)options
context:(void *)context
{
[self forEachProperty:^(objc_property_t prop, NSString *name) {
[self addObserver:observer
forKeyPath:name
options:options
context:context];
}];
}
-(void)removeObserver:(id)observer {
[self forEachProperty:^(objc_property_t prop, NSString *name) {
[self removeObserver:observer forKeyPath:name];
}];
}
@end