objective-c 使用 valueForKeyPath 获取数组元素

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1461126/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-03 22:14:46  来源:igfitidea点击:

Getting array elements with valueForKeyPath

objective-ccocoakey-value-coding

提问by jsd

Is there any way to access an NSArrayelement with valueForKeyPath? Google's reverse geocoder service, for example, returns a very complex data structure. If I want to get the city, right now I have to break it into two calls, like this:

有没有办法访问一个NSArray元素valueForKeyPath?例如,Google 的反向地理编码器服务返回一个非常复杂的数据结构。如果我想得到这个城市,现在我必须把它分成两个调用,像这样:

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

Just wondering if there's a way to shoehorn the objectAtIndex:call into the valueForKeyPathstring. I tried a javascript-esque formulation like @"Placemark[0].address" but no dice.

只是想知道是否有办法将objectAtIndex:呼叫硬塞到valueForKeyPath字符串中。我尝试了像 @"Placemark[0].address" 这样的 javascript-esque 公式,但没有骰子。

采纳答案by Alex

Unfortunately, no. The full documentation for what's allowed using Key-Value Coding is here. There are not, to my knowledge, any operators that allow you to grab a particular array or set object.

抱歉不行。使用键值编码允许的内容的完整文档在这里。据我所知,没有任何运算符允许您获取特定数组或设置对象。

回答by psy

Here's a category I just wrote for NSObject that can handle array indexes so you can access a nested object like this: "person.friends[0].name"

这是我刚刚为 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

Use it like so

像这样使用它

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

There's no error checking, so it's prone to breaking but you get the idea.

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

回答by Graham Perks

You can intercept the keypath in the object holding the NSArray.

您可以截取保存 NSArray 的对象中的键路径。

In your case the keypath would become Placemark0.address... Override valueForUndefinedKey; look for the index in the keypath; something like this:

在您的情况下,keypath 将变为 Placemark0.address... Override valueForUndefinedKey; 在键路径中查找索引;像这样:

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

This works really well for model frameworks e.g. Mantle.

这对于模型框架非常有效,例如Mantle

回答by Sparky

Subclass NSArrayController or NSDictionaryController

子类 NSArrayController 或 NSDictionaryController

Use NSArrayControllerfor this purpose, because NSObjectControllerdoes not include NSArrayController's provided handling of changes to bound array elements. If you use this same code with NSObjectControllerinstead, then using Cocoa Bindings with your NSObjectControllerinstance will only set the (bound interface element's) value at the time of binding but will not receive the messages from array elements in return. By using NSObjectControllerfor this purpose, the user interface will not continue to update even though the contentObjectis updated. Simply use the same code with NSArrayControllerto also include proper support for arrays -- which is the matter at hand.

使用NSArrayController用于此目的,因为NSObjectController不包括NSArrayController“s提供处理变为结合数组元素。如果您使用相同的代码NSObjectController代替,那么在您的NSObjectController实例中使用 Cocoa Bindings只会在绑定时设置(绑定接口元素的)值,但不会从数组元素接收消息作为回报。通过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

This code uses NSRegularExpression, which is for macOS 10.7+. I leave it as an exercise for you to use the same approach to also override setValueForKeyPath, if you want write functionality.

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



Cocoa Bindings Example Usage

可可绑定示例用法

Say we want a little trivia game, with a window that shows a question and uses four buttons to display multiple-choice options. We have the questions and multiple-choice options as NSStrings in a plist, and also an NSNumberor optionally BOOLentries to indicate the correct answers. We want to bind the option buttons to options in the array, for each question also stored in an array.

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

Here is the example plist containing some trivia questions related to the game Halo. Notice that the options are located within nested arrays.

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

Trivia Property List

琐事属性列表

In this example, I use NSObjectController *stringsControlleras the controller for the entire plist file, and DelvingArrayController *triviaControlleras the controller for the trivia-related plist entries. You might simply use one DelvingArrayControllerinstead, but I provide this for your understanding.

在这个例子中,我NSObjectController *stringsController用作整个 plist 文件DelvingArrayController *triviaController的控制器,以及与琐事相关的 plist 条目的控制器。你可以简单地使用一个DelvingArrayController来代替,但我提供这个是为了你的理解。

The trivia window is really simple, so I merely design it using Interface Builder in MainMenu.xib:

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

Trivia Window in Interface Builder

Interface Builder 中的琐事窗口

Trivia Interface Builder Bindings

琐事界面生成器绑定

A subclass of NSDocumentControlleris used for showing the trivia window via an NSMenuItemadded in Interface Builder. The instance of this subclass is also in the .xib, so if we want to use the interface elements in the .xib, we have to wait for the Application Delegate instance's - (void)applicationDidFinishLaunching:(NSNotification *)aNotificationmethod or otherwise wait until the .xib has finished loading...

的子类NSDocumentController用于通过NSMenuItem在 Interface Builder 中添加的来显示琐事窗口。这个子类的实例也在 .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


Summary of Sequence

序列总结

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