UI Design by Ditko (part 2)

This is the next entry in a series of posts covering the various open source frameworks available on Kosoku's GitHub.

This post will finish covering Ditko, a UI-centric framework available on iOS, tvOS, watchOS, and macOS. We will cover the classes available in the framework.

All of the images in this post were generated using the Demo-iOS target in the Ditko repo.

KDIWindow

KDIWindow.h is a UIWindow subclass that allows for an accessory view to be placed at the top or bottom edge of the window, similar to the in call bar on iPhone. For example:

#import <Ditko/Ditko.h>

// assume this exists
UIView *view = ...;
// from the root view controller of the application
KDIWindow *window = ...;

// by default the window will layout the accessory view at the top edge
window.accessoryView = view;

You get something that looks like this:

KDIWindow.png

KDIBadgeView

KDIBadgeView.h is a UIView subclass that draws a badge, similar to the system badging of tab bar items. It is very customizable, provides properties to change the foreground and background colors in normal and highlighted state as well as font, edge insets, and corner radius. For example:

#import <Ditko/Ditko.h>

KDIBadgeView *view = [[KDIBadgeView alloc] initWithFrame:CGRectZero];

// configure the badge view to draw like the system white on red badges
view.badgeForegroundColor = UIColor.whiteColor;
view.badgeBackgroundColor = UIColor.redColor;
view.badge = @"Badged!";

You get something that looks like this:

KDIBadgeView.png

KDIButton

KDIButton.h is a UIButton subclass that provides a variety of additional methods, including setting different alignments for image and title. For example, you could set image to be top alignment and centered horizontally, while setting the title to be bottom aligned and centered horizontally. Something like this:

#import <Ditko/Ditko.h>

KDIButton *button = [KDIButton buttonWithType:UIButtonTypeSystem];
// assume this exists
UIImage *image = ...;

button.titleContentVerticalAlignment = KDIButtonContentVerticalAlignmentBottom;
button.titleContentHorizontalAlignment = KDIButtonContentHorizontalAlignmentCenter;
button.imageContentVerticalAlignment = KDIButtonContentVerticalAlignmentTop;
button.imageContentHorizontalAlignment = KDIButtonContentHorizontalAlignmentCenter;
[button setImage:image forState:UIControlStateNormal];
[button setTitle:@"Title" forState:UIControlStateNormal];

You get a button that looks like this:

KDIButton.png

KDIBadgeButton

KDIBadgeButton.h is a UIView subclass that manages an instance of KDIButton and KDIBadgeView to allow for badging of system elements similar to UITabBarItem. For example:

#import <Ditko/Ditko.h>

KDIBadgeButton *badgeButton = [[KDIBadgeButton alloc] initWithFrame:CGRectZero];
// assume this exists
UIImage *image = ...;

badgeButton.badgePosition = KDIBadgeButtonBadgePositionRelativeToImage;
badgeButton.badgeView.badge = @"64";
badgeButton.button.imageContentVerticalAlignment = KDIButtonContentVerticalAlignmentTop;
badgeButton.button.imageContentHorizontalAlignment = KDIButtonContentHorizontalAlignmentCenter;
badgeButton.button.titleContentVerticalAlignment = KDIButtonContentVerticalAlignmentBottom;
badgeButton.button.titleContentHorizontalAlignment = KDIButtonContentHorizontalAlignmentCenter;
[badgeButton.button setImage:image forState:UIControlStateNormal];
[badgeButton.button setTitle:@"Badge Button!" forState:UIControlStateNormal];

You would get a view that looks like this:

KDIBadgeButton.png

KDIEmptyView

KDIEmptyView.h is a UIView subclass that can be used to provide information to the user when content for a view is empty or unavailable. For example, you want to show the user's photos but require their permission first, this view could prompt the user to approve the system permission alert or prompt them to approve the permission in Settings if it was denied the first time. Sensible defaults are provided for font, color and spacing. For example:

#import <Ditko/Ditko.h>

KDIEmptyView *view = [[KDIEmptyView alloc] initWithFrame:CGRectZero];
// assume this exists
UIImage *image = ...;

view.image = image;
view.headline = @"Empty Headline";
view.body = @"Empty body text";
view.action = @"Toggle activity indicator";

You get a view that looks like this:

KDIEmptyView.png

KDIGradientView

KDIGradientView.h is a UIView subclass that uses CAGradientLayer as its backing layer to draw a gradient. For example:

#import <Ditko/Ditko.h>

KDIGradientView *gradientView = [[KDIGradientView alloc] initWithFrame:CGRectZero];

gradientView.colors = @[KDIColorRandomHSB(),
                             KDIColorRandomHSB(),
                             KDIColorRandomHSB()];
gradientView.startPoint = CGPointMake(0, 0);
gradientView.endPoint = CGPointMake(1, 1);

You get a view that looks like this:

KDIGradientView.png

KDITextField

KDITextField.h is a UITextField subclass that provides inset support for the text, left, and right views. For example:

#import <Ditko/Ditko.h>

CGFloat kSubviewMargin = 8.0;
// assume textField is a property defined on self
self.textField = [[KDITextField alloc] initWithFrame:CGRectZero];
self.textField.textEdgeInsets = UIEdgeInsetsMake(kSubviewMargin, kSubviewMargin, kSubviewMargin, kSubviewMargin);
self.textField.leftViewEdgeInsets = UIEdgeInsetsMake(kSubviewMargin, kSubviewMargin, kSubviewMargin, 0);
self.textField.leftView = ({
    // assume this exists
    UIImage *leftImage = ...;
    UIImageView *retval = [[UIImageView alloc] initWithImage:leftImage];

    retval.tintColor = UIColor.lightGrayColor;

    retval;
});
self.textField.leftViewMode = UITextFieldViewModeAlways;
self.textField.rightViewEdgeInsets = UIEdgeInsetsMake(kSubviewMargin, 0, kSubviewMargin, kSubviewMargin);
self.textField.rightView = ({
    // assume this exists
    UIImage *rightImage = ...;
    UIButton *retval = [UIButton buttonWithType:UIButtonTypeSystem];

    [retval setImage:rightImage forState:UIControlStateNormal];
    [retval sizeToFit];

    retval;
});
self.textField.rightViewMode = UITextFieldViewModeAlways;

You get a text field that looks like this:

KDITextField.png

KDIProgressNavigationBar

KDIProgressNavigationBar.h is a UINavigationBar subclass that manages a UIProgressView instance you can message using category methods on UINavigationController. You must ensure the UINavigationController you are referencing is using KDIProgressNavigationBar as its navigation bar class using the initWithNavigationBarClass:toolbarClass: initializer method.

For example, inside your UIViewController subclass:

#import <Ditko/Ditko.h>

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];

    self.navigationController.KDI_progressNavigationBar.progress = 0.2;
    [self.navigationController.KDI_progressNavigationBar setProgressHidden:NO animated:animated];
}

Would look like this:

KDIProgressNavigationBar.png

KDIDatePickerButton

KDIDatePickerButton.h is a KDIButton subclass that manages a UIDatePicker as its inputView. It provides the same customization that UIDatePicker does as well as a few additions. For example:

#import <Ditko/Ditko.h>

// assume this exists
UIImage *image = ...;
// assume datePickerButton is a property defined on self
self.datePickerButton = [KDIDatePickerButton buttonWithType:UIButtonTypeSystem];
self.datePickerButton.titleEdgeInsets = UIEdgeInsetsMake(0, kSubviewMargin, 0, 0);
[self.datePickerButton setImage:image forState:UIControlStateNormal];
self.datePickerButton.dateTitleBlock = ^NSString * _Nullable(__kindof KDIDatePickerButton * _Nonnull datePickerButton, NSString * _Nonnull defaultTitle) {
    return [NSString stringWithFormat:@"Due Date: %@",defaultTitle];
};

You would get this:

KDIDatePickerButton.png

KDIPickerViewButton

KDIPickerViewButton.h is a KDIButton subclass that manages a UIPickerView as its inputView. It provides the same customization that UIPickerView does through its KDIPickerViewButtonDataSource and KDIPickerViewButtonDelegate protocols, as well as a few additions. For example:

- (void)viewDidLoad {
    [super viewDidLoad];

    // assume rowsAndComponents is a property defined on self
    self.rowsAndComponents = @[@[@"Red",@"Green",@"Blue"],
                               @[@"One",@"Two",@"Three"]];

    // assume this exists
    UIImage *image = ...;
    // assume pickerViewButton is an IBOutlet on self
    self.pickerViewButton.titleEdgeInsets = UIEdgeInsetsMake(0, kSubviewMargin, 0, 0);
    [self.pickerViewButton setImage:image forState:UIControlStateNormal];
    self.pickerViewButton.selectedComponentsJoinString = @", ";
    self.pickerViewButton.dataSource = self;
    self.pickerViewButton.delegate = self;
}

- (NSInteger)numberOfComponentsInPickerViewButton:(KDIPickerViewButton *)pickerViewButton {
    return self.rowsAndComponents.count;
}
- (NSInteger)pickerViewButton:(KDIPickerViewButton *)pickerViewButton numberOfRowsInComponent:(NSInteger)component {
    return self.rowsAndComponents[component].count;
}
- (NSString *)pickerViewButton:(KDIPickerViewButton *)pickerViewButton titleForRow:(NSInteger)row forComponent:(NSInteger)component {
    return self.rowsAndComponents[component][row];
}

You get this:

KDIPickerViewButton.png

KDITableViewCell

KDITableViewCell.h is a UITableViewCell subclass that supports a variety of commonly used configurations. It provides methods to customize its appearance. In its default state, it looks like this:

KDITableViewCell.png

KDITextView

KDITextView.h is a UITextView subclass that provides placeholder support along with min/max number of lines and height support. This is what it looks like with a multiple line placeholder set:

KDITextView.png

KDIProgressSlider

KDIProgressSlider.h is a UISlider subclass that can display loading progress. For example a scrubber control used with a streaming video player. It's default configuration looks like this:

KDIProgressSlider.png

That does it for part 2 of the Ditko blog posts. Once again, you can find all the code behind the screen shots in this post in the Ditko repo. Thanks for reading!

UI Design by Ditko

Welcome back for the next post in a series highlighting the various frameworks available on Kosoku's GitHub.

Next up is Ditko, a UI-centric framework available on iOS, tvOS, watchOS, and macOS. Ditko is a much larger framework than Stanley, so covering Ditko will be split up into 2 posts. This post will cover the macros, functions, protocols, and categories available in the framework. Part 2 will cover the classes available in the framework. Let's get started!

Macros

KDIColorMacros.h provides platform specific macros to create colors in various color spaces. For example:

// creates a color with the provided RGB components
UIColor *color = KDIColorRGB(0.2,0.5,1.0);
// creates a color from the hex string, including the leading @"#" character is optional
UIColor *colorFromHex = KDIColorHexadecimal(@"#abcd");

Functions

KDIFunctions.h provides functions to scale a CGSize based on the scale factor of the main screen. For example:

// this would be (50,50) on a retina display and (75,75) on a 3x display (e.g. iPad Pro)
CGSize size = KDICGSizeAdjustedForMainScreenScale(CGSizeMake(25,25));

Protocols

KDIBorderedView.h is a protocol adopted by a number of views within Ditko that allows the view to draw borders with varying options. For example, using KDIView, which conforms to KDIBorderedView, you could do the following:

// assume borderView is a property defined on self
self.borderView = [[KDIView alloc] initWithFrame:CGRectZero];

self.borderView.backgroundColor = KDIColorW(0.95);

self.borderView.borderOptions = KDIBorderOptionsTopAndBottom;
self.borderView.borderWidth = 4.0;
self.borderView.borderColor = KDIColorRandomRGB();

[self.view addSubview:self.borderView];

// setup your auto layout constraints

You would get something that looks like this:

KDIView.png

Categories

UIColor+KDIExtensions.h and NSColor+KDIExtensions.h provide methods to create and manipulate colors. You can create random RGB and HSB colors, create colors from hexadecimal strings, and create colors by adjusting the hue, saturation, or balance of an existing color. For example:

// creates a random RGB color
UIColor *randomRGB = UIColor.KDI_colorRandomRGB;
// creates a random HSB color
UIColor *randomHSB = UIColor.KDI_colorRandomHSB;
// creates a color from a hexadecimal string, the leading @"#" is optional
UIColor *colorFromHex = [UIColor KDI_colorWithHexadecimalString:@"#abcd"];
// this will be @"#abcd"
NSString *hexFromColor = [colorFromHex KDI_hexadecimalString];
// will be either white or black, whichever is more readable
UIColor *contrastColor = [colorFromHex KDI_contrastingColor];
// creates a color that is 10 percent brighter than randomRGB
UIColor *brightColor = [randomRGB KDI_colorByAdjustingBrightnessByPercent:0.1];

UIImage+KDIExtensions.h and NSImage+KDIExtensions.h provide methods to quickly switch between original and template images, and determine the dominant color of an image. For example:

// assume this exists
UIImage *image = ...;
// this will be a template image
UIImage *templateImage = image.KDI_templateImage;
// the dominant color of image, use with the methods above to determine which colors are most readable when layered on top the image
UIColor *color = [image KDI_dominantColor];

UIBarButtonItem+KDIExtensions.h provides methods to create some commonly used bar button items as well as attach blocks to bar button items instead of the normal target/action pattern. For example:

// need Stanley for the weakify/strongify macros
#import <Stanley/Stanley.h>

weakify(self);
UIBarButtonItem *addItem = [UIBarButtonItem KDI_barButtonSystemItem:UIBarButtonSystemItemAdd style:UIBarButtonItemStylePlain block:^(__kindof UIBarButtonItem *barButtonItem){
    strongify(self);
    // this block is invoked whenever the bar button item is tapped
    [self foo];
}];

// you can also set the block property on any created UIBarButtonItem, doing so will override its target/action
addItem.KDI_block = ^(__kindof UIBarButtonItem *barButtonItem) {
    strongify(self);
    [self bar];
};

UIControl+KDIExtensions.h and NSControl+KDIExtensions.h provide methods to attach blocks in addition to the normal target/action pattern. For example, on iOS:

// need Stanley for the weakify/strongify macros
#import <Stanley/Stanley.h>

// assume this exists, UIButton is a subclass of UIControl
UIButton *button = ...;

weakify(self);
[button KDI_addBlock:^(__kindof UIControl *control, UIControlEvents controlEvents){
    strongify(self);
    // this block will be invoked whenever the specified control events take place
    [self foo];
} forControlEvents:UIControlEventTouchUpInside];

Similarly, on macOS:

// need Stanley for the weakify/strongify macros
#import <Stanley/Stanley.h>

// assume this exists, NSButton is a subclass of NSControl
NSButton *button = ...;

weakify(self);
button.KDI_block = ^(__kindof NSControl *control) {
    strongify(self);
    // this block will be invoked whenever the control action is triggered
    [self bar];
};

UIGestureRecognizer+KDIExtensions.h and NSGestureRecognizer+KDIExtensions.h provide methods to attach blocks in addition to the normal target/action pattern. For example, on iOS:

// need Stanley for the weakify/strongify macros
#import <Stanley/Stanley.h>

// assume this exists
UITapGestureRecognizer *recognizer = ...;

weakify(self);
[recognizer KDI_addBlock:^(__kindof UIGestureRecognizer *gestureRecognizer){
    strongify(self);
    // this block is invoked each time the gesture recognizer state changes
    [self foo];
}];

Similarly, on macOS:

// need Stanley for the weakify/strongify macros
#import <Stanley/Stanley.h>

// assume this exists
NSClickGestureRecognizer *recognizer = ...;

weakify(self);
recognizer.KDI_block = ^(__kindof NSGestureRecognizer *gestureRecognizer) {
    strongify(self);
    // this block is invoked each time the gesture recognizer state changes
    [self bar];
};

UINavigationController+KDIExtensions.h provides methods to push and pop view controllers that take an optional completion block that will be invoked when the animation is finished. For example:

// assume this exists
UIViewController *push = ...;

[self.navigationController KDI_pushViewController:push animated:YES completion:^{
    // this block is invoked when the push animation completes
    [self foo];
}];

UIViewController+KDIExtensions.h provides methods to determine the correct view controller for presentation, recursively dismiss all view controllers and present view controllers as a popover. For example:

// assume this exists
UIViewController *present = ...;
// assume self is a UIViewController
[[self KDI_viewControllerForPresenting] presentViewController:present animated:YES completion:nil];

UIAlertController+KDIExtensions.h provides methods for easily presenting UIAlertController instances while providing an optional completion block. Sensible defaults are provided for all options. For example:

// assume this is an error from a networking call
NSError *error = ...;

// determines the correct view controller to present on and performs the presentation
[UIAlertController KDI_presentAlertControllerWithError:error];

UIFont+KDIDynamicTypeExtensions.h provides methods that allow any control to easily support dynamic type. It also allows dynamic type support for custom fonts by allowing the client to provide its own method that supplies the fonts that should be used when dynamic type changes. For example:

// assume this exists, built in text classes already support the required KDIDynamicTypeObject protocol
UILabel *label = ...;

// one line is all it takes!
label.KDI_dynamicTypeTextStyle = UIFontTextStyleBody;

Or in your custom class:

#import <Ditko/Ditko.h>

// with this setup, setFont: will be called whenever there are dynamic changes
// MyClass would then update its relevant text display with the new font
@class MyClass : NSObject <KDIDynamicTypeObject>
@property (strong,nonatomic) UIFont *font;
@end

@implementation MyClass
- (SEL)dynamicTypeSetFontSelector {
    return @selector(setFont:);
}
@end

UIViewController+KDIUIImagePickerControllerExtensions.h provides methods to present a UIImagePickerController instance in the appropriate manner as well supply a completion block that will be invoked when the user makes a selection or takes a photo/video. For example:

#import <Ditko/Ditko.h>

// assume this exists
UIImagePickerController *controller = ...;

[[UIViewController KDI_viewControllerForPresenting] KDI_presentImagePickerController:controller barButtonItem:nil sourceView:nil sourceRect:CGRectZero permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES completion:^(NSDictionary<NSString *,id> * _Nullable info){
    UIImage *image = info.KDI_image;

    // do something with the image
}];

Meet Stanley

This is the first in a series of posts covering the variety of open source frameworks available on Kosoku's GitHub.

First up is Stanley, a framework which mainly extends the Foundation framework and is available on iOS, tvOS, watchOS, and macOS. This post will highlight various facilities available in the framework.

Macros

KSTScopeMacros.h provides a few useful macros to avoid typing out key paths and avoid those tricky retain cycles when dealing with blocks. For example:

// this will throw a compiler error if name isn't defined on self
NSString *nameKeyPath = @kstKeypath(self,name);

// assume block is a property defined on self
kstWeakify(self);
self.block = ^{
    kstStrongify(self);
    // you can refer to self normally without creating a retain cycle
    self.name = @"John Wick";
};

Functions

KSTGeometryFunctions.h provides functions to manipulate CGRect and NSRect structures. For example:

CGRect rect1 = ...;
CGRect rect2 = ...;

// center rect1 within rect2
CGRect centerRect = KSTCGRectCenterInRect(rect1, rect2);

Categories

The NSData+KSTExtensions.h category provides methods to easily get the cyrptographic hash of the receiver's bytes. Methods are provided for MD5, SHA1, SHA256, and SHA512 string hashes. For example:

NSData *data = ...;
NSString *MD5Hash = data.KST_MD5String;

// do something with your hashed string

The NSFileManager+KSTExtensions.h category provides methods to get directory URLs for commonly accessed locations, like Application Support, Caches, and Documents. For example:

NSURL *directoryURL = NSFileManager.defaultManager.KST_applicationSupportDirectoryURL;

// do something with the directory URL

The NSHTTPURLResponse+KSTExtensions.h category provides constants for all the HTTP response codes which you can use to check against response codes and errors returned by network requests. For example:

NSHTTPURLResponse *response = ...; // assume this exists

if (response.statusCode == KSTHTTPStatusCodeCreated) {
    // something was created, act accordingly
}

Classes

KSTDirectoryWatcher.h provides a wrapper around the FSEvents framework on macOS. You provide it an array of directory URLs, some options, and a block. It will invoke that block whenever anything happens within any of the directories you provided upon initialization. For example:

NSArray<NSURL *> *URLs = ...;
KSTDirectoryWatcher *watcher = [[KSTDirectoryWatcher alloc] initWithURLs:URLs flags:KSTDirectoryWatcherEventFlagsItemCreated block:^(KSTDirectoryWatcher *directoryWatcher, KSTDirectoryWatcherEventID eventID, KSTDirectoryWatcherEventFlags flags, NSURL *URL){
    // a file was created, scan the directory
}];

// this tells `FSEvents` to start monitoring the directory URLs
[watcher startWatchingURLs];

KSTFileWatcher.h provides a wrapper around Grand Central Dispatch file sources and is available on all the platforms (iOS/tvOS/watchOS/macOS). You supply it an array of file URLs, some options, and a block. It will invoke that block whenever any of the files are modified. For example:

NSArray<NSURL *> *URLs = ...;
KSTFileWatcher *watcher = [[KSTFileWatcher alloc] initWithURLs:URLs flags:KSTFileWatcherFlagsWrite|KSTFileWatcherFlagsExtend|KSTFileWatcherFlagsRename block:^(KSTFileWatcher *fileWatcher, NSURL *URL, KSTFileWatcherFlags flags){
    // the file at URL was written to or renamed, act accordingly
}];

KSTPhoneNumberFormatter.h is an NSFormatter subclass that generates nicely formatted phone numbers, even as the user types (and on iOS, if you use another handy Kosoku library KSOTextValidation). It is locale aware, but falls back to en_US formatting if necessary. For example:

NSString *phoneNumber = @"1234567890";
NSString *formatted = [KSTPhoneNumberFormatter.sharedFormatter localizedStringFromPhoneNumber:phoneNumber];

NSLog(@"%@",formatted); // prints @"(123) 456-7890"

KSTReachabilityManager.h provides a wrapper around the network reachability APIs that are part of the SystemConfiguration framework on iOS/tvOS/macOS. It can monitor the reachability of network resources and post notifications when things change. You can use the sharedManager for generic reachability or use one of the init methods to create your own instance for a specific network resource. For example:

KSTReachabilityManager *reachability = [[KSTReachabilityManager alloc] initWithDomain:@"https://myawesomedomain.com"];

[reachability startMonitoringReachability];

// listen for KSTReachabilityManagerNotificationDidChangeStatus to see when things change

KSTTimer.h aims to be a replacement for NSTimer, it uses Grand Central Dispatch and doesn't retain its target like NSTimer does. It supplies methods that match the API provided by NSTimer as well as block support.

// timer is a property defined on self
self.timer = [KSTTimer scheduledTimerWithTimeInterval:1.0 block:^(KSTimer *timer){
    // this block is invoked on each timer fire
} userInfo:nil repeats:YES queue:nil];

Those are just a few of the highlights. Stay tuned for additional articles in the series highlighting Kosoku frameworks!