objective c - Obteniendo elementos de matriz con valueForKeyPath
objective-c cocoa (4)
Subclase NSArrayController o NSDictionaryController
Use NSArrayController
para este propósito, porque NSObjectController
no incluye el manejo proporcionado por NSArrayController
de los cambios a los elementos de la matriz enlazada. Si utiliza este mismo código con NSObjectController
en NSObjectController
lugar, entonces usar Cocoa Bindings con su instancia de NSObjectController
solo establecerá el valor (elemento de interfaz vinculado) en el momento del enlace pero no recibirá los mensajes de los elementos de la matriz a cambio. Al utilizar NSObjectController
para este propósito, la interfaz de usuario no continuará actualizándose aunque el contentObject
esté actualizado. Simplemente use el mismo código con NSArrayController
para incluir también el soporte adecuado para los arreglos, que es el asunto en cuestión.
#import <Cocoa/Cocoa.h>
@interface DelvingArrayController : NSArrayController
@end
#import "DelvingArrayController.h"
@implementation DelvingArrayController
-(id)valueForKeyPath:(NSString *)keyPath
{
NSError *error = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^(.+?)//[(//d+?)//]$" options:NSRegularExpressionCaseInsensitive error:&error];
NSArray<NSString*> *components = [keyPath componentsSeparatedByString:@"."];
id currentObject = self;
for (NSUInteger i = 0; i < components.count; i++)
{
if (![components[i] isEqualToString:@""])
{
NSTextCheckingResult *check_result = [regex firstMatchInString:components[i] options:0 range:NSMakeRange(0, components[i].length)];
if (!check_result)
currentObject = [currentObject valueForKey:components[i]];
else
{
NSRange array_name_capture_range = [check_result rangeAtIndex:1];
NSRange number_capture_range = [check_result rangeAtIndex:2];
if (number_capture_range.location == NSNotFound)
currentObject = [currentObject valueForKey:components[i]];
else if (array_name_capture_range.location != NSNotFound)
{
NSString *array_name = [components[i] substringWithRange:array_name_capture_range];
NSUInteger array_index = [[components[i] substringWithRange:number_capture_range] integerValue];
currentObject = [currentObject valueForKey:array_name];
if ([currentObject count] > array_index)
currentObject = [currentObject objectAtIndex:array_index];
}
}
}
}
return currentObject;
}
//at some point... also override setValueForKeyPath :-)
@end
Este código utiliza NSRegularExpression
, que es para macOS 10.7+. Lo dejo como un ejercicio para que uses el mismo enfoque para anular también setValueForKeyPath
, si quieres la funcionalidad de escritura.
Ejemplo de enlaces de cacao
Digamos que queremos un pequeño juego de preguntas, con una ventana que muestra una pregunta y utiliza cuatro botones para mostrar opciones de opción múltiple. Tenemos las preguntas y opciones de opción múltiple como NSString
s en una lista, y también un NSNumber
u opcionalmente BOOL
entradas para indicar las respuestas correctas. Queremos vincular los botones de opción a las opciones de la matriz, para cada pregunta también almacenada en una matriz.
Aquí está la lista de ejemplos que contiene algunas preguntas de trivia relacionadas con el juego Halo . Observe que las opciones están ubicadas dentro de matrices anidadas.
En este ejemplo, uso NSObjectController *stringsController
como controlador para todo el archivo plist, y DelvingArrayController *triviaController
como controlador para las entradas de plist relacionadas con trivia. En DelvingArrayController
lugar, simplemente puede usar un DelvingArrayController
, pero le proporciono esto para su comprensión.
La ventana de trivia es realmente simple, así que simplemente la diseño utilizando Interface Builder en MainMenu.xib:
Se utiliza una subclase de NSDocumentController
para mostrar la ventana de trivia a través de un NSMenuItem
agregado en Interface Builder. La instancia de esta subclase también se encuentra en el .xib, por lo que si queremos usar los elementos de la interfaz en el .xib, tenemos que esperar a la instancia del delegado de la aplicación - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
método o de lo contrario esperar Hasta que el .xib haya terminado de cargar ...
#import <Cocoa/Cocoa.h>
#import "MenuInterfaceDocumentController.h"
@interface AppDelegate : NSObject <NSApplicationDelegate>
@property IBOutlet MenuInterfaceDocumentController *PrimaryInterfaceController;
@end
#import "AppDelegate.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
@synthesize PrimaryInterfaceController;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
if ([NSApp mainMenu])
{
[PrimaryInterfaceController configureTriviaWindow];
}
}
#import <Cocoa/Cocoa.h>
@interface MenuInterfaceDocumentController : NSDocumentController
{
IBOutlet NSMenuItem *MenuItemTrivia; // shows the Trivia window
IBOutlet NSWindow *TriviaWindow;
IBOutlet NSTextView *TriviaQuestionField;
IBOutlet NSButton *TriviaOption1, *TriviaOption2, *TriviaOption3, *TriviaOption4;
}
@property NSObjectController *stringsController;
-(void)configureTriviaWindow;
@end
#import "MenuInterfaceDocumentController.h"
@interface MenuInterfaceDocumentController ()
@property NSDictionary *languageDictionary;
@property DelvingArrayController *triviaController;
@property NSNumber *triviaAnswer;
@end
@implementation MenuInterfaceDocumentController
@synthesize stringsController, languageDictionary, triviaController, triviaAnswer;
// all this happens before the MainMenu is available, and before the AppDelegate is sent applicationDidFinishLaunching
-(instancetype)init
{
self = [super init];
if (self)
{
if (!stringsController)
stringsController = [NSObjectController new];
stringsController.editable = NO;
// check for the plist file, eventually applying the following
languageDictionary = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"en" ofType:@"plist"]];
if (languageDictionary)
[stringsController setContent:languageDictionary];
if (!triviaController)
{
triviaController = [DelvingArrayController new];
[triviaController bind:@"contentArray" toObject:stringsController withKeyPath:@"selection.trivia" options:nil];
}
triviaController.editable = NO;
if (!triviaAnswer)
{
triviaAnswer = @0;
[self bind:@"triviaAnswer" toObject:triviaController withKeyPath:@"selection.answer" options:nil];
}
}
return self;
}
// if we ever do something like change the plist file to a duplicate plist file that is in a different language, use this kind of approach to keep the same trivia entry active
-(IBAction)changeLanguage:(id)sender
{
NSUInteger triviaQIndex = triviaController.selectionIndex;
if (sender == MenuItemEnglishLanguage)
{
if ([self changeLanguageTo:@"en" Notify:YES])
{
[self updateSelectedLanguageMenuItemWithLanguageString:@"en"];
if ([triviaController.content count] > triviaQIndex) // in case the plist files don''t match
[triviaController setSelectionIndex:triviaQIndex];
}
else
[self displayAlertFor:CUSTOM_ALERT_TYPE_LANGUAGE_CHANGE_FAILED];
}
else if (sender == MenuItemGermanLanguage)
{
if ([self changeLanguageTo:@"de" Notify:YES])
{
[self updateSelectedLanguageMenuItemWithLanguageString:@"de"];
if ([triviaController.content count] > triviaQIndex)
[triviaController setSelectionIndex:triviaQIndex];
}
else
[self displayAlertFor:CUSTOM_ALERT_TYPE_LANGUAGE_CHANGE_FAILED];
}
}
-(void)configureTriviaWindow
{
[TriviaQuestionField bind:@"string" toObject:triviaController withKeyPath:@"selection.question" options:nil];
[TriviaOption1 bind:@"title" toObject:triviaController withKeyPath:@"selection.options[0]" options:nil];
[TriviaOption2 bind:@"title" toObject:triviaController withKeyPath:@"selection.options[1]" options:nil];
[TriviaOption3 bind:@"title" toObject:triviaController withKeyPath:@"selection.options[2]" options:nil];
[TriviaOption4 bind:@"title" toObject:triviaController withKeyPath:@"selection.options[3]" options:nil];
}
// this method is how you would manually set the value if you did not use binding:
-(void)updateTriviaAnswer
{
triviaAnswer = [triviaController valueForKeyPath:@"selection.answer"];
}
-(IBAction)changeTriviaQuestion:(id)sender
{
if (triviaController.selectionIndex >= [(NSArray*)triviaController.content count] - 1)
[triviaController setSelectionIndex:0];
else
[triviaController setSelectionIndex:(triviaController.selectionIndex + 1)];
}
-(IBAction)showTriviaWindow:(id)sender
{
[TriviaWindow makeKeyAndOrderFront:sender];
}
- (IBAction)TriviaOptionChosen:(id)sender
{
// tag integers 0 through 3 are assigned to the option buttons in Interface Builder
if ([sender tag] == triviaAnswer.integerValue)
[self changeTriviaQuestion:sender];
else
NSBeep();
}
@end
Resumen de la secuencia
NSObjectController *stringsController = [[NSObjectController alloc] initWithContent:[NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"en" ofType:@"plist"]]];
DelvingArrayController *triviaController = [DelvingArrayController new];
[triviaController bind:@"contentArray" toObject:stringsController withKeyPath:@"selection.trivia" options:nil];
NSNumber *triviaAnswer = @0;
[self bind:@"triviaAnswer" toObject:triviaController withKeyPath:@"selection.answer" options:nil];
// bind to .xib''s interface elements after the nib has finished loading, else the IBOutlets are null
[TriviaQuestionField bind:@"string" toObject:triviaController withKeyPath:@"selection.question" options:nil];
[TriviaOption1 bind:@"title" toObject:triviaController withKeyPath:@"selection.options[0]" options:nil];
[TriviaOption2 bind:@"title" toObject:triviaController withKeyPath:@"selection.options[1]" options:nil];
[TriviaOption3 bind:@"title" toObject:triviaController withKeyPath:@"selection.options[2]" options:nil];
[TriviaOption4 bind:@"title" toObject:triviaController withKeyPath:@"selection.options[3]" options:nil];
// when the user chooses the correct option, go to the next question
if ([sender tag] == triviaAnswer.integerValue)
{
if (triviaController.selectionIndex >= [(NSArray*)triviaController.content count] - 1)
[triviaController setSelectionIndex:0];
else
[triviaController setSelectionIndex:(triviaController.selectionIndex + 1)];
}
¿Hay alguna forma de acceder a un elemento NSArray
con valueForKeyPath
? El servicio de geocodificador inverso de Google, por ejemplo, devuelve una estructura de datos muy compleja. Si quiero obtener la ciudad, ahora mismo tengo que dividirla en dos llamadas, como esta:
NSDictionary *address = [NSString stringWithString:[[[dictionary objectForKey:@"Placemark"] objectAtIndex:0] objectForKey:@"address"]];
NSLog(@"%@", [address valueForKeyPath:@"AddressDetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.LocalityName"]);
Solo me pregunto si hay una manera de objectAtIndex:
el objectAtIndex:
call en la cadena valueForKeyPath
. Intenté una formulación de javascript-esque como @ "Placemark [0] .address" pero no dados.
Aquí hay una categoría que acabo de escribir para NSObject que puede manejar índices de matriz para que pueda acceder a un objeto anidado como este: "person.friends [0] .name"
@interface NSObject (ValueForKeyPathWithIndexes)
-(id)valueForKeyPathWithIndexes:(NSString*)fullPath;
@end
#import "NSObject+ValueForKeyPathWithIndexes.h"
@implementation NSObject (ValueForKeyPathWithIndexes)
-(id)valueForKeyPathWithIndexes:(NSString*)fullPath
{
NSRange testrange = [fullPath rangeOfString:@"["];
if (testrange.location == NSNotFound)
return [self valueForKeyPath:fullPath];
NSArray* parts = [fullPath componentsSeparatedByString:@"."];
id currentObj = self;
for (NSString* part in parts)
{
NSRange range1 = [part rangeOfString:@"["];
if (range1.location == NSNotFound)
{
currentObj = [currentObj valueForKey:part];
}
else
{
NSString* arrayKey = [part substringToIndex:range1.location];
int index = [[[part substringToIndex:part.length-1] substringFromIndex:range1.location+1] intValue];
currentObj = [[currentObj valueForKey:arrayKey] objectAtIndex:index];
}
}
return currentObj;
}
@end
Utilízalo como tal
NSString* personsFriendsName = [obj valueForKeyPathsWithIndexes:@"me.friends[0].name"];
No hay comprobación de errores, por lo que es propenso a romperse, pero entiendes la idea.
Lamentablemente no. La documentación completa de lo que está permitido utilizando la codificación de valor-clave está here . No hay, según mi conocimiento, ningún operador que le permita tomar una matriz o un objeto determinado.
Puede interceptar la ruta de acceso clave en el objeto que contiene el NSArray.
En su caso, la ruta de acceso clave se convertiría en Placemark0.address ... Anular valueForUndefinedKey; buscar el índice en el camino de acceso; algo como esto:
-(id)valueForUndefinedKey:(NSString *)key
{
// Handle paths like Placemark0, Placemark1, ...
if ([key hasPrefix:@"Placemark"])
{
// Caller wants to access the Placemark array.
// Find the array index they''re after.
NSString *indexString = [key stringByReplacingOccurrencesOfString:@"Placemark" withString:@""];
NSInteger index = [indexString integerValue];
// Return array element.
if (index < self.placemarks.count)
return self.placemarks[index];
}
return [super valueForUndefinedKey:key];
}
Esto funciona muy bien para marcos de modelos, por ejemplo, Mantle .