Huvi: Passcode Screen

The Huvi design called for a passcode screen for controlling access to parent mode and child profiles. Based on the designer’s mockups, this screen was to be translucent teal with white buttons, and a slight blur on the background. There were several opportunities to make this either a clean, reusable, maintainable module of code, or else a complete mess of copy-and-paste nonsense. I (hopefully) succeeded at making it the former.

huvi-passcode-screen

Functionally, the passcode screen just needed to accept N digits of numeric input from the user, inform them if they were wrong, let them in if they were right, and let them exit if they wanted. There’s a bit more to it than that (like the ability to authenticate with your account password if you’re a parent but can’t remember your numeric passcode), but other than that, pretty straightforward.

Aesthetically, the fade-in animation and blurred blue background both come along for the ride by subclassing HVNotify, the Huvi class I wrote for displaying modal messages and dialogs – this is definitely a place where good class design paid dividends. What makes the HVPasscode class neat is its factory for producing the UI elements in code, and the dead-simple interface the application uses to call it. From the perspective of the View Controller code that invokes the HVPasscode screen, here’s all it takes:

[HVPasscode checkChildModePasscodeForProfile:profile].then(^(NSNumber* validated){
    if (validated.boolValue) {
        // We use the sender variable to pass the profile we want to show
        [self performSegueWithIdentifier:@"showChildMode" sender:self.profiles[indexPath.row]];
    }
});

This is PromiseKit-wrapped, but a block callback would have been just as simple. The idea is that, with a single class method, we can create a new passcode view, display it modally (while also ensuring it’s the ONLY passcode view we display), accept and process user input, and simply tell the original caller whether the user is authorized or not.

The other simplifying bit was to create a factory method for making the actual buttons, and using some autolayout magic to programmatically create the dozens of constraints needed for the view. The numpad buttons subclass UIButton, but do their own custom drawing to create the circle at the correct radius.

Setting up the grid looks like this:

-(instancetype)initCommonHVNumpadView {
    NSMutableDictionary* tempButtons = [[NSMutableDictionary alloc] initWithCapacity:10];
    // need these in NSString:UIView format for autolayout
    NSMutableDictionary* views = [[NSMutableDictionary alloc] initWithCapacity:self.buttons.count];

    // num keys 0-9
    for (NSInteger i = 0; i < 10; i++) {
        UIButton* button = [HVNumpadButtonView numPadKeyForNumber:i];
        [button addTarget:self action:@selector(pressedButton:) forControlEvents:UIControlEventTouchUpInside];
        [tempButtons setObject:button forKey:@(i)];
        [self addSubview:button];
       
        NSString* name = [NSString stringWithFormat:@"b%ld", (long)i];
        [views setObject:button forKey:name];
    }
    
    // delete key
    UIButton* deleteButton = [HVNumpadButtonView numPadKeyForNumber:kDeleteKeyTag];
    [deleteButton addTarget:self action:@selector(pressedButton:) forControlEvents:UIControlEventTouchUpInside];
    deleteButton.imageView.contentMode = UIViewContentModeCenter;
    [self addSubview:deleteButton];
    [tempButtons setObject:deleteButton forKey:@(kDeleteKeyTag)];
    [views setObject:deleteButton forKey:@"delete"];
    
    self.buttons = [NSDictionary dictionaryWithDictionary:tempButtons];
    
    // Spacer
    UIView* spacer1 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];
    spacer1.translatesAutoresizingMaskIntoConstraints = NO;
    [self addSubview:spacer1];
    views[@"s1"] = spacer1;
    
    // Layout
    NSArray* formatStrings = @[@"H:|[b1][b2(==b1)][b3(==b1)]|",
                               @"H:|[b4][b5(==b4)][b6(==b4)]|",
                               @"H:|[b7][b8(==b7)][b9(==b7)]|",
                               @"H:|[s1][b0(==s1)][delete(==s1)]|",
                               
                               @"V:|[b1][b4(==b1)][b7(==b1)][s1(==b1)]|",
                               @"V:|[b2][b5(==b2)][b8(==b2)][b0(==b2)]|",
                               @"V:|[b3][b6(==b3)][b9(==b3)][delete(==b3)]|"
                               ];
    
    for (NSString* formatString in formatStrings) {
        [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:formatString options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:views]];
    }
    [self setNeedsLayout];
    
    return self;
}

Step one is to create all the buttons we’ll need and stuff them into dictionaries in the format we’ll need. We create names for the buttons as well, to make the layout code more readable.

Next, an array of format strings – one for each row and column – sets up the layout of the view, using Cocoa’s autolayout visual grammar.

Finally, a loop iterates over the layout constraint strings, produces constraints from them, and adds the constraints to the view, setting up for a layout pass the next time it’s needed.