Получение элементов массива с помощью valueForKeyPath - PullRequest
20 голосов
/ 22 сентября 2009

Есть ли способ получить доступ к элементу 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. Я пробовал формулировку в стиле javascript, например @ "Placemark [0] .address", но не игра в кости.

Ответы [ 4 ]

18 голосов
/ 22 сентября 2009

К сожалению, нет. Полная документация для того, что разрешено с помощью Key-Value Coding: здесь . Насколько мне известно, нет никаких операторов, которые бы позволяли вам захватывать определенный массив или заданный объект.

16 голосов
/ 07 декабря 2012

Вот категория, которую я только что написал для 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"];

Проверка ошибок отсутствует, поэтому она может быть взломана, но вы поймете, что это за идея.

3 голосов
/ 30 сентября 2013

Вы можете перехватить путь к ключу в объекте, содержащем NSArray.

В вашем случае ключевой путь станет 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];
}

Это очень хорошо работает для каркасов моделей, например Мантия .

0 голосов
/ 30 апреля 2018

Подкласс NSArrayController или NSDictionaryController

Используйте NSArrayController для этой цели, поскольку NSObjectController не включает предоставленную NSArrayController обработку изменений связанных элементов массива. Если вместо этого вы используете этот же код с NSObjectController, то при использовании привязки Какао с вашим экземпляром 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, если вы хотите написать функциональность.


Пример использования привязок какао

Допустим, нам нужна небольшая викторина с окном, в котором отображается вопрос, и для отображения вариантов с несколькими вариантами используются четыре кнопки. У нас есть вопросы и варианты с несколькими вариантами ответов, например NSString s в листе, а также NSNumber или, необязательно, BOOL записей для указания правильных ответов. Мы хотим связать кнопки параметров с параметрами в массиве, для каждого вопроса, также сохраненного в массиве.

Вот примерный список, содержащий некоторые мелочи, связанные с игрой Halo . Обратите внимание, что параметры расположены во вложенных массивах.

Trivia Property List

В этом примере я использую NSObjectController *stringsController в качестве контроллера для всего файла plist, а DelvingArrayController *triviaController в качестве контроллера для записей plist, связанных с пустяками. Вы могли бы просто использовать вместо этого DelvingArrayController, но я предоставлю это для вашего понимания.

Окно пустяков действительно простое, поэтому я просто создаю его с помощью Interface Builder в MainMenu.xib:

Trivia Window in Interface Builder

Trivia Interface Builder Bindings

Подкласс NSDocumentController используется для отображения окна викторин с помощью NSMenuItem, добавленного в Interface Builder. Экземпляр этого подкласса также находится в .xib, поэтому, если мы хотим использовать элементы интерфейса в .xib, нам нужно дождаться метода - (void)applicationDidFinishLaunching:(NSNotification *)aNotification экземпляра Application Delegate или иным образом дождаться завершения загрузки .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)];
}
...