使用valueForKeyPath获取数组元素

时间:2009-09-22 16:24:56

标签: objective-c cocoa key-value-coding

有没有办法通过NSArray访问valueForKeyPath元素?例如,Google的反向地理编码器服务会返回一个非常复杂的数据结构。如果我想要进城,那么现在我必须把它分成两个电话,如下:

NSDictionary *address = [NSString stringWithString:[[[dictionary objectForKey:@"Placemark"] objectAtIndex:0] objectForKey:@"address"]];
NSLog(@"%@", [address valueForKeyPath:@"AddressDetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.LocalityName"]);

只是想知道是否有办法将objectAtIndex:号召入valueForKeyPath字符串。我尝试了像@“Placemark [0] .address”这样的javascript-esque配方,但没有骰子。

5 个答案:

答案 0 :(得分:18)

不幸的是,没有。使用键值编码允许的完整文档是here。据我所知,没有任何允许你抓取特定数组或设置对象的运算符。

答案 1 :(得分:16)

这里是我刚为NSObject编写的一个类别,它可以处理数组索引,因此您可以访问这样的嵌套对象:" 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

像这样使用

NSString* personsFriendsName = [obj valueForKeyPathsWithIndexes:@"me.friends[0].name"];

没有错误检查,所以它很容易破裂,但你明白了。

答案 2 :(得分:3)

您可以拦截持有NSArray的对象中的keypath。

在你的情况下,keypath将成为Placemark0.address ...覆盖valueForUndefinedKey;在keypath中查找索引;像这样的东西:

-(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];
}

这非常适用于模型框架,例如Mantle

答案 3 :(得分:0)

子类NSArrayController或NSDictionaryController

出于此目的使用NSArrayController,因为NSObjectController未包含NSArrayController提供的对绑定数组元素更改的处理。如果您使用与NSObjectController相同的代码,那么将Cocoa绑定与NSObjectController实例一起使用,只会在绑定时设置(绑定的界面元素)值,但不会接收来自数组元素的消息。通过使用NSObjectController来实现此目的,即使contentObject已更新,用户界面也不会继续更新。只需使用与NSArrayController相同的代码,也可以包含对数组的适当支持 - 这是当前的事情。

#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

此代码使用NSRegularExpression,适用于macOS 10.7+。 如果你想要写功能,我把它作为练习让你使用相同的方法覆盖setValueForKeyPath

Cocoa Bindings示例用法

假设我们想要一个小琐事游戏,有一个显示问题的窗口,并使用四个按钮来显示多项选择。我们在plist中有NSString个问题和多项选项,还有NSNumber或可选的BOOL条目来指示正确的答案。 我们希望将选项按钮绑定到数组中的选项,同样存储在数组中的每个问题。

以下是包含与游戏 Halo 相关的一些琐事问题的示例plist。请注意,这些选项位于嵌套数组中。

Trivia Property List

在此示例中,我使用NSObjectController *stringsController作为整个plist文件的控制器,并使用DelvingArrayController *triviaController作为与琐事相关的plist条目的控制器。您可以简单地使用一个DelvingArrayController,但我提供此信息是为了您的理解。

琐事窗口非常简单,所以我只是在MainMenu.xib中使用Interface Builder进行设计:

Trivia Window in Interface Builder

Trivia Interface Builder Bindings

NSDocumentController的子类用于通过Interface Builder中添加的NSMenuItem显示琐事窗口。这个子类的实例也在.xib中,所以如果我们想要使用.xib中的接口元素,我们必须等待Application Delegate实例的- (void)applicationDidFinishLaunching:(NSNotification *)aNotification方法或者等到.xib已经完成加载......

#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

序列摘要

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)];
}

答案 4 :(得分:0)

为 NSObject 创建支持数组的方法:

@interface NSObject(ArraySupported)
-(id)valueForKeySupportedArray:(NSString*)path;
-(id)valueForKeyPathSupportedArray:(NSString*)fullPath;
@end

@implementation NSObject(ArraySupported)
-(id)valueForKeySupportedArray:(NSString*)path {
    id value = nil;
    if ([self isKindOfClass:[NSArray class]]) {
        NSArray *array = (NSArray *)self;
        NSUInteger index = path.integerValue;
        if (index >= 0 && index < array.count) {
            value = array[index];
        }
    } else {
        value = [self valueForKey:path];
    }
    return value;
}

-(id)valueForKeyPathSupportedArray:(NSString*)fullPath {
    NSArray* parts = [fullPath componentsSeparatedByString:@"."];
    id value = self;
    for (NSString* part in parts) {
        value = [value valueForKeySupportedArray:part];
        if (value == nil) {
            break;
        }
    }
    return value;
}
@end

使用方法:

NSObject *object = @{@"Placemark":@[@{@"address":@"..."}]};
NSString *address = [object valueForKeyPathSupportedArray:@"Placemark.0.address"];
// address = "..."