Search code examples
objective-cmacoscocoacocoa-bindings

Cocoa Binding for bit mask property uses check boxes


What is the best way to use Cocoa bindings with checkbox NSButton's and an integer property used as a bit mask?

I have defined this enum for the days of the week:

typedef enum {
    DayOfWeekSun = 1 << 0,
    DayOfWeekMon = 1 << 1,
    DayOfWeekTue = 1 << 2,
    DayOfWeekWed = 1 << 3,
    DayOfWeekThu = 1 << 4,
    DayOfWeekFri = 1 << 5,
    DayOfWeekSat = 1 << 6
} DaysOfWeek;

And I have a property defined on my model (in Core Data, but it doesn't matter):

@property NSInteger days;

So, for example this property might be set to Sunday and Wednesday as follows:

model.days = DayOfWeekSun | DayOfWeekWed;

In my NIB I have created checkboxes for Sunday, Monday, Tuesday, etc. and I want to use Cocoa binding to bind these checkboxes to the days property.

This is my first try, experimenting a bit. It almost works, but altering the value in the check boxes isn't reflected in the model.

@interface WYBDaysOfWeekBitMask : NSObject

@property (nonatomic) NSInteger daysOfWeek;
@property BOOL monday;
@property BOOL tuesday;
@property BOOL wednesday;
@property BOOL thursday;
@property BOOL friday;
@property BOOL saturday;
@property BOOL sunday;

@end

@implementation DaysOfWeekBitMask

- (void)setDaysOfWeek:(NSInteger)daysOfWeek
{
    [self willChangeValueForKey:@"daysOfWeek"];
    _daysOfWeek = daysOfWeek;
    [self didChangeValueForKey:@"daysOfWeek"];
}

- (BOOL)sunday
{
    return _daysOfWeek & DayOfWeekSun;
}

- (void)setSunday:(BOOL)sunday
{
    [self willChangeValueForKey:@"sunday"];
    [self setDayOfWeek:sunday dayBitMask:DayOfWeekSun];
    [self didChangeValueForKey:@"sunday"];
}

[... and so on for the other days ...]

- (void)setDayOfWeek:(BOOL)selected dayBitMask:(DaysOfWeek)mask
{
    [self willChangeValueForKey:@"daysOfWeek"];
    _daysOfWeek = _daysOfWeek & ~mask;
    if (selected) {
        _daysOfWeek = _daysOfWeek | mask;
    }
    [self didChangeValueForKey:@"daysOfWeek"];
}

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    NSSet *set = nil;

    if ([key isEqualToString:@"daysOfWeek"]) {
        set = [NSSet setWithObjects:@"sunday", @"monday", @"tuesday", @"wednesday", @"thursday", @"friday", @"saturday", nil];
    }
    else if ([key isEqualToString:@"sunday"] ||
             [key isEqualToString:@"monday"] ||
             [key isEqualToString:@"tuesday"] ||
             [key isEqualToString:@"wedneday"] ||
             [key isEqualToString:@"thursday"] ||
             [key isEqualToString:@"friday"] ||
             [key isEqualToString:@"saturday"]) {
        set = [NSSet setWithObject:@"daysOfWeek"];
    }

    return set;
}

@end

And this is set as follows in my NIB controller:

-(void)loadView
{
    [super loadView];

    // Manual Bindings
    self.daysOfWeek = [WYBDaysOfWeekBitMask new];
    [self.daysOfWeek bind:@"daysOfWeek" toObject:self.timesArrayController withKeyPath:@"selection.days" options:nil];
}

I have a feeling I am going about this the wrong way. Any help would be much appreciated.


Solution

  • It's times like these when KVO seems really powerful. Here's an example of a way you can use KVO to add makeshift properties to your object.

    AppDelegate.h

    #import <Cocoa/Cocoa.h>
    
    // use NS_OPTIONS instead of a typedef enum for bit masks
    typedef NS_OPTIONS(NSUInteger, DayOptions) {
        DaySunday = 1 << 0,
        DayMonday = 1 << 1,
        DayTuesday = 1 << 2,
        DayWednesday = 1 << 3,
        DayThursday = 1 << 4,
        DayFriday = 1 << 5,
        DaySaturday = 1 << 6,
    };
    
    @interface AppDelegate : NSObject <NSApplicationDelegate, NSOutlineViewDataSource, NSOutlineViewDelegate>
    @property (assign) DayOptions days;
    @end
    

    AppDelegate.m

    #import "AppDelegate.h"
    
    DayOptions optionsForString(NSString *dayString) {
        NSString *day = dayString.lowercaseString;
    
        if ([day isEqualToString:@"sunday"]) return DaySunday;
        else if ([day isEqualToString:@"monday"]) return DayMonday;
        else if ([day isEqualToString:@"tuesday"]) return DayTuesday;
        else if ([day isEqualToString:@"wednesday"]) return DayWednesday;
        else if ([day isEqualToString:@"thursday"]) return DayThursday;
        else if ([day isEqualToString:@"friday"]) return DayFriday;
        else if ([day isEqualToString:@"saturday"]) return DaySaturday;
        return 0;
    }
    
    @implementation AppDelegate
    
    // return an NSNumber representing the presence of a bit mask
    - (id)valueForUndefinedKey:(NSString *)key {
        DayOptions options = optionsForString(key);
        if (options != 0) {
            return @((BOOL)(self.days & options));
        }
        return [super valueForKey:key];
    }
    
    // a NO value will remove the mask from -days and a YES value will add it
    - (void)setValue:(id)value forUndefinedKey:(NSString *)key {
        DayOptions options = optionsForString(key);
        if (options != 0 && [value isKindOfClass:[NSNumber class]]) {
            BOOL flag = [value boolValue];
            if (flag)
                self.days |= options;
            else
                self.days &= ~options;
        } else {
            [super setValue:value forUndefinedKey:key];
        }
    }
    // send KVO notifications whenever -days gets changed
    + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
        if ([key isEqualToString:@"sunday"] ||
            [key isEqualToString:@"monday"] ||
            [key isEqualToString:@"tuesday"] ||
            [key isEqualToString:@"wednesday"] ||
            [key isEqualToString:@"thursday"] ||
            [key isEqualToString:@"friday"] ||
            [key isEqualToString:@"saturday"]) {
            return [NSSet setWithObject:NSStringFromSelector(@selector(days))];
        }
        return [super keyPathsForValuesAffectingValueForKey:key];
    }
    
    @end
    

    This code is untested but should compile and I think it'd work well

    So you would add a Days property (I renamed your DaysOfWeek to Days) to whatever class is in charge of your nib, and bind each checkbox to sunday, monday, etc. properties on that. The class uses KVO methods to return real values for unknown key paths and will also send KVO notifications when -days gets changed.

    So there is no need to create an extra class to wrap around your enum.