Sheeeeeeeeet is a UIKit library that lets you create menus that can be presented as custom action sheets, context menus, alert controllers, or in any way you like.

The result can look like this or completely different:

customize UIActionSheet
UIActionSheet
SwiftUI UIActionSheet
custom Action Sheet

Sheeeeeeeeet comes with many item types (standard items, buttons, titles, toggles, etc.) and can be extended with your own custom item types.

About this repository

Since I have personally moved over to SwiftUI, this repository is no longer under active development. I will however gladly merge any PRs that add value to it or fixes problems with new iOS versions.

Installation

Sheeeeeeeeet can be installed with the Swift Package Manager:

https://github.com/danielsaidi/Sheeeeeeeeet.git

or with CocoaPods:

pod Sheeeeeeeeet

If you prefer to not have external dependencies, you can also just copy the source code into your app.

Supported Platforms

Sheeeeeeeeet supports iOS 9 and later.

Getting Started

Creating a menu

With Sheeeeeeeeet, you start with creating a menu, like this:

let item1 = MenuItem(title: "Int", value: 1)
let item2 = MenuItem(title: "Car", value: Car())
let button = OkButton(title: "OK")
let items = [item1, item2, button]
let menu = Menu(title: "Select a type", items: items)

The library has many built-in item types, e.g. buttons, select items, links, etc. A complete list can be found here.

Menu Items

With Sheeeeeeeeet, you can create Menus that can be used in various ways, e.g. in iOS 13 context menus, custom action sheets and native alert controllers.

Menus contains menu items of various types, which are divided into itemsbuttons and titles. You can also create custom item types by subclassing any of the built-in types.

Menu items are converted to different types dependinc on where they are used. For instance, custom action sheets will convert them to ActionSheetItemCells, while native action sheets can convert them to actions.

Menu Item Types

Items are used to present options. Sheeeeeeeeet has the following built-in types:

  • MenuItem – A standard menu item with a title, optional subtitle, image etc.
  • SelectItem – A menu item that can be marked as isSelected and styled accordingly.
  • SingleSelectItem – A select item that deselects all other single-select items in the same group.
  • MultiSelectItem – A select item that doesn’t deselect other select items.
  • MultiSelectItemToggle – An item that selects/deselects all multi-select items in the same group.
  • LinkItem – A standard menu item that renders as a link.
  • SecondaryActionItem – A menu item that also has a secondary action.
  • CollectionItem – A generic item that can contain any type of items (e.g. collection view cells).
  • Custom Item – A generic item that can use any item (e.g. custom views).

All items, including the buttons and items below, inherit MenuItem. Each item has a tapBehavior, which determines how it should behave when tapped.

Buttons

Buttons are used to apply or discard an action sheet. Sheeeeeeeeet has the following built-in types:

  • MenuButton – A standard button with no default behavior
  • OkButton – A standard ok/apply button
  • CancelButton – A standard cancel button
  • DestructiveButton – A destructive ok/apply button

OK and destructive buttons have MenuButton.ButtonType.ok as value, while cancel buttons have MenuButton.ButtonType.cancel. You can also check a button’s type e.g. button is OkButton.

Different presentations handle buttons differently. Custom action sheets automatically separates buttons into a separate group, while popovers just ignore cancel buttons.

Titles

Titles are non-interactive text or spacing elements. Sheeeeeeeeet has the following built-in types:

  • MenuTitle – A main menu item
  • SectionTitle – Can be used to indicate that some items belong together
  • SectionMargin – Can be used to add spacing before and after a section title

You can add title items anywhere, although some presentations may ignore them. Custom action sheets will also add a title item to the menu if it has a title, so don’t use MenuTitle directly.

Header Views

ActionSheet has a headerView property, which will be displayed as a floating header above the action sheet. You can use any view as a header view.

ActionSheet also has a headerViewConfiguration with which you can setup how the header view should behave in various scenarios e.g. in landscape, popovers etc.

Custom Items

Besides the built-in CustomItem, which is pretty complicated to setup, you can also create custom items that inherits any of the built-in item types. You can read more about custom menu items here.

Custom Menu Items

Sheeeeeeeeet can be extended with custom menu item types.

If you want to use entire custom views, you can use the built-in CustomItem to use custom UIViews in a custom action sheet. The demo app shows you how to do this.

However, CustomItem can be pretty complicated to setup and requires custom xib files.

Instead, you can inherit any of the built-in item types and make it use any custom item cell when you present them in a custom action sheet.

For instance, the custom type below inherits the standard MenuItem base class and makes it use another cell type:

class MultilineItem: MenuItem {

    override func actionSheetCell(for tableView: UITableView) -> ActionSheetItemCell {
        DemoMultilineItemCell(style: actionSheetCellStyle)
    }
    
    override var actionSheetCellType: ActionSheetItemCell.Type {
        DemoMultilineItemCell.self
    }
}

The custom cell type inherits the standard ActionSheetItemCell base class and enables multiline support when it’s refreshed:

class MultilineItemCell: ActionSheetItemCell {
    
    override func refresh() {
        super.refresh()
        textLabel?.numberOfLines = 0
    }
}

Then you can adjust the cell’s appearance to make it appear taller than other cells

class CustomAppearance: ActionSheetAppearance {
    
    override func apply() {
        super.apply()
        MultilineItemCell.appearance().height = 100
    }

You can test this custom item in the demo app.

You can also create your own custom item types by inheriting any of the existing ones. For instance, if you build a car rental app, you can create a car-specific item that takes a Car model.

Presenting a menu as an action sheet

You can present menus as custom action sheets:

let sheet = menu.toActionSheet(...) { sheet, item in ... }
sheet.present(in: vc, from: view) { sheet, item in ...
    print("You selected \(item.title)")
}

You can find more information in this action sheet guide.

Action Sheets

Sheeeeeeeeet Menus can be presented as custom action sheets.

Custom action sheets are by default presented from the bottom of the screen on iPhone and in a popover on iPad. They support all menu item types and provides rich customization and styling possibilities.

How to present a Menu as an ActionSheet

When you have a Menu instance, you can create an ActionSheet with toActionSheet(...):

let item1 = MenuItem(title: "Option 1")
let item2 = MenuItem(title: "Option 2")
let cancel = CancelButton(title: "Cancel")
let menu = Menu(title: "My menu", items: [item1, item2, cancel])
let sheet = menu.toActionSheet { sheet, item in ...
    print("You selected \(item.title)")
}

You can also create an ActionSheet instance with the standard initializer:

let sheet = ActionSheet(menu: menu) { sheet, item in
    if let value = item.value as? Int { print("You selected an int: \(value)") }
    if let value = item.value as? Car { print("You selected a car") }
    if item is OkButton { print("You tapped the OK button") }
}

You can provide a custom configuration and headerConfiguration when you create an action sheet with either of these two methods. You can also provide a custom presenter.

You can then present the action sheet by using any of its present functions, for instance:

sheet.present(in: vc, from: button) {
    print("Action sheet was presented")
}

The sheet will be presented according to the sheet’s configuration, presenter and appearance.

Custom types

You can subclass MenuMenuItem and ActionSheet. This makes it possible to create app-specific menus and action sheets that provide specific functionality, handle specific tasks etc. Have a look at the demo app for examples.

Styling

Sheeeeeeeeet‘s ActionSheet can be extensively styled beyond their standard appearance. You can apply custom colors, fonts, margins etc. by modifying the appearance proxies of the various action sheet item cells, e.g. ActionSheetSelectItemCell.

See this appearance and styling guide for more information or have a look at the demo app.

Action Sheet Appearance

Sheeeeeeeeet‘s ActionSheet is very customizable and lets you change the appearance of its views and items, e.g. heights, fonts, colors, margins etc. It uses appearance proxies, so you can change the style of all instances of a certain item type, as well as individual instances.

This guide will show you the available customization alternatives. To learn more, check out the demo app and this advanced example.

Global appearance

Sheeeeeeeeet will automatically apply a standard appearance when the first action sheet is presented, if no custom appearance has been applied before that.

To apply a custom appearance, create a class that inherits ActionSheetAppearance, for instance:

class MyCustomAppearance: ActionSheetAppearance {
    override func apply() {
        super.apply()
        let view = ActionSheetTableView.appearance()
        view.backgroundColor = .red
    }
}

You can then apply the appearance as a global appearance like this:

ActionSheet.applyAppearance(MyCustomAppearance())

Now, Sheeeeeeeeet will not apply a standard appearance when the next sheet is presented, since a custom global appearance has been applied.

ActionSheet appearances

The ActionSheet class has some appearance properties that applies to the action sheet’s layout:

  • minimumContentInsets: UIEdgeInsets (the minimum screen edge margins)
  • preferredPopoverWidth: CGFloat (the popover width, when presented on iPads)
  • sectionMargins: CGFloat (the distance between the header, items and buttons)

These properties currenly apply to each action sheet instance, so you currently can’t change the default values for all action sheets instances in your app.

ActionSheet subview appearances

The ActionSheet class has several subviews that can be globally styled, e.g. ActionSheetBackgroundView. To modify their appearances, just use their appearance proxies:

ActionSheetBackgroundView.appearance().backgroundColor = .purple
ActionSheetTableView.appearance().cornerRadius = 15
ActionSheetButtonTableView.appearance().cornerRadius = 20 // Otherwise 15
ActionSheetHeaderContainerView.appearance().cornerRadius = 15

ActionSheetItemCell appearances

MenuItems are mapped to ActionSheetItemCells (or subclasses) when they are presented in an action sheet. To modify their appearances, just modify the cells’ appearance proxies:

ActionSheetItemCell.appearance().titleColor = .red
ActionSheetOPkButtonCell.appearance().titleColor = .red

Appearance proxy properties are inherited, so if you change ActionSheetItemCell.appearance().titleColor, all subclasses get the same color if you don’t override it for a certain subclass.

Action sheet item heights

The default action sheet cell height is 50 points, with 25 for some title items. You can set a custom height for every item type, like this:

ActionSheetItemCell.appearance().height = 60
ActionSheetSectionMarginCell.appearance().height = 30

NOTE Cell heights are not applied like other appearance properties, since it’s not inherited. You must customize each cell type individually.

Advanced Example

When you have the basics under control, check out this advanced example to see how you can take things further. It goes through advanced styling and customizations.

You can also find basic and more advanced action sheet examples in the demo app.

Don’t hesitate to contact me if you any need help with implementing powerful, custom action sheets in your apps.

Apply a global action sheet appearance

The code below applies a global style to all items, then overrides it for some items. Note that it for simplicity doesn’t use dark mode supporting colors (the demo app does).

let item = ActionSheetItemCell.appearance()
item.height = 50
item.titleColor = .darkGray
item.titleFont = .systemFont(ofSize: 15)
item.subtitleColor = .lightGray
item.subtitleFont = .systemFont(ofSize: 13)
item.titleColor = .darkGray

let title = ActionSheetTitleCell.appearance()
title.height = 60
title.titleColor = .black
title.titleFont = .systemFont(ofSize: 16)
title.separatorInset = .hiddenSeparator

let sectionTitle = ActionSheetSectionTitleCell.appearance()
sectionTitle.height = 20
sectionTitle.titleColor = .black
sectionTitle.titleFont = .systemFont(ofSize: 12)
sectionTitle.subtitleFont = .systemFont(ofSize: 12)
sectionTitle.separatorInset = .hiddenSeparator

let selectItem = ActionSheetSelectItemCell.appearance()
selectItem.selectedIcon = UIImage(named: "ic_checkmark")
selectItem.unselectedIcon = UIImage(named: "ic_empty")

let singleSelectItem = ActionSheetSingleSelectItemCell.appearance()
singleSelectItem.selectedTitleColor = .black

let multiSelectItem = ActionSheetMultiSelectItemCell.appearance()
multiSelectItem.selectedTitleColor = .green

let toggleItem = ActionSheetMultiSelectToggleItemCell.appearance()
toggleItem.height = 20
toggleItem.titleColor = .black
toggleItem.titleFont = .systemFont(ofSize: 12)
toggleItem.subtitleFont = .systemFont(ofSize: 12)
toggleItem.separatorInset = .hiddenSeparator
toggleItem.selectAllSubtitleColor = .darkGray
toggleItem.deselectAllSubtitleColor = .red

let linkItem = ActionSheetLinkItemCell.appearance()
linkItem.linkIcon = UIImage(named: "ic_arrow_right")

let button = ActionSheetButtonCell.appearance()
button.titleFont = .systemFont(ofSize: 15)

let okButton = ActionSheetOkButtonCell.appearance()
okButton.titleColor = .black

let cancelButton = ActionSheetCancelButtonCell.appearance()
cancelButton.titleFont = .systemFont(ofSize: 13)
cancelButton.titleColor = .lightGray

Changing the style of an individual cell works in the same way:

let button = ActionSheetOkButton(title: "OK")   // Button font size is "15"
button.titleFont = .systemFont(ofSize: 17)      // Instance font size is now "17"

Creating the action sheet

With the styling above, we can now create a pretty complex action sheet:

enum Time { case morning, afternoon }
enum Service { case cleaning, oiling, waxing }

let title = ActionSheetTitle(title: "Bike Service")
let section1 = ActionSheetSectionTitle(title: "Time", subtitle: "Pick one")
let item1_1 = ActionSheetSingleSelectItem(title: "Morning", subtitle: "08:00 - 12:00", isSelected: true, group: "time", value: Time.morning, tapBehavior: .none)
let item1_2 = ActionSheetSingleSelectItem(title: "Afternoon", subtitle: "13:00 - 17:00", isSelected: false, group: "time", value: Time.afternoon, tapBehavior: .none)
let margin1 = ActionSheetSectionMargin()
let section2 = ActionSheetMultiSelectToggleItem(title: "Services", state: .selectAll, group: "services", selectAllTitle: "Select all", deselectAllTitle: "Deselect all")
let item2_1 = ActionSheetMultiSelectItem(title: "Cleaning", subtitle: "$50", isSelected: true, group: "services", value: Service.cleaning)
let item2_2 = ActionSheetMultiSelectItem(title: "Oiling", subtitle: "$25", isSelected: false, group: "services", value: Service.oiling)
let item2_3 = ActionSheetMultiSelectItem(title: "Waxing", subtitle: "$25", isSelected: false, group: "services", value: Service.waxing)
let ok = ActionSheetOkButton(title: "Book Servixce")
let cancel = ActionSheetCancelButton(title: "Cancel")
let items = [title, section1, item1_1, item1_2, margin1, section2, item2_1, item2_2, item2_3, ok, cancel]
let sheet = ActionSheet(items: items) { sheet, item in
    guard item is OkButton else { return }
    let times = sheet.items.compactMap { $0 as? ActionSheetSingleSelectItem }
    let selectedTime = times.first { $0.isSelected }
    let services = sheet.items.compactMap { $0 as? ActionSheetMultiSelectItem }
    let selectedServices = services.filter { $0.isSelected }
    print("Place order with the selected time and services")
}

You can now present this sheet just like the sheet in the basic example. It will behave in the same way.

Subclassing action sheet

If you were to present this action sheet in multiple places in your app, you may want to consider creating a specific action sheet type. In this case, the action sheet is a bike service order sheet, so let’s work with that.

We can easily do this by creating an ActionSheet subclass, like this:

enum Time { case morning, afternoon }
enum Service { case cleaning, oiling, waxing }

class BikeServiceActionSheet: ActionSheet {
    
    init(action: @escaping SelectAction) {
        let items = BikeServiceActionSheet.createItems()
        super.init(items: items, action: action)
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    var selectedTime: Time? {
        let items = self.items.compactMap { $0 as? ActionSheetSingleSelectItem }
        let selected = items.first { $0.isSelected }
        return selected?.value as? Time
    }
    
    var selectedServices: [Service] {
        let items = self.items.compactMap { $0 as? ActionSheetMultiSelectItem }
        let selected = items.filter { $0.isSelected }
        return selected.compactMap { $0.value as? Service }
    }
}

private extension BikeServiceActionSheet {
    
    static func createItems() -> [ActionSheetItem] {
        let title = ActionSheetTitle(title: "Bike Service")
        let section1 = ActionSheetSectionTitle(title: "Time", subtitle: "Pick one")
        let item1_1 = ActionSheetSingleSelectItem(title: "Morning", subtitle: "08:00 - 12:00", isSelected: true, group: "time", value: Time.morning, tapBehavior: .none)
        let item1_2 = ActionSheetSingleSelectItem(title: "Afternoon", subtitle: "13:00 - 17:00", isSelected: false, group: "time", value: Time.afternoon, tapBehavior: .none)
        let margin1 = ActionSheetSectionMargin()
        let section2 = ActionSheetMultiSelectToggleItem(title: "Services", state: .selectAll, group: "services", selectAllTitle: "Select all", deselectAllTitle: "Deselect all")
        let item2_1 = ActionSheetMultiSelectItem(title: "Cleaning", subtitle: "$50", isSelected: true, group: "services", value: Service.cleaning)
        let item2_2 = ActionSheetMultiSelectItem(title: "Oiling", subtitle: "$25", isSelected: false, group: "services", value: Service.oiling)
        let item2_3 = ActionSheetMultiSelectItem(title: "Waxing", subtitle: "$25", isSelected: false, group: "services", value: Service.waxing)
        let ok = ActionSheetOkButton(title: "Book Servixce")
        let cancel = ActionSheetCancelButton(title: "Cancel")
        return [title, section1, item1_1, item1_2, margin1, section2, item2_1, item2_2, item2_3, ok, cancel]
    }
}

With this in place, creating, presenting and handling the action sheet is easy:

let sheet = BikeServiceActionSheet() { sheet, item in
    guard item is OkButton else { return }
    guard let sheet = sheet as? BikeServiceActionSheet else { return }
    let selectedTime = sheet.selectedTime
    let selectedServices = sheet.selectedServices
    print("Place order with the selected time and services")
}

The above code is much better than the original one! We now have a custom action sheet that sets up its own items and provides us with additional properties that makes it a lot easier to work with the sheet.

Subclassing action sheet items

Subclassing item types gives you more fine-grained control over the action sheet and its content. It also makes it possible to customize the appearances for your custom item types as well.

We can simplify the action sheet above even more, by creating two new item types:

class TimeItem: ActionSheetMultiSelectItem {
    
    var time: Time? {
        return value as? Time
    }
}

class ServiceItem: ActionSheetSingleSelectItem {
    
    var service: Service? {
        return value as? Service
    }
}

We can now use the new types in the action sheet, to simplify the code even more:

class BikeServiceActionSheet: ActionSheet {
    
    init(action: @escaping SelectAction) {
        let items = BikeServiceActionSheet.createItems()
        super.init(items: items, action: action)
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    var selectedTime: Time? {
        let items = self.items.compactMap { $0 as? TimeItem }
        let selected = items.first { $0.isSelected }
        return selected?.time
    }
    
    var selectedServices: [Service] {
        let items = self.items.compactMap { $0 as? ServiceItem }
        let selected = items.filter { $0.isSelected }
        return selected.compactMap { $0.service }
    }
}

private extension BikeServiceActionSheet {
    
    static func createItems() -> [ActionSheetItem] {
        let title = ActionSheetTitle(title: "Bike Service")
        let section1 = ActionSheetSectionTitle(title: "Time", subtitle: "Pick one")
        let item1_1 = TimeItem(title: "Morning", subtitle: "08:00 - 12:00", isSelected: true, group: "time", value: Time.morning, tapBehavior: .none)
        let item1_2 = TimeItem(title: "Afternoon", subtitle: "13:00 - 17:00", isSelected: false, group: "time", value: Time.afternoon, tapBehavior: .none)
        let margin1 = ActionSheetSectionMargin()
        let section2 = ActionSheetMultiSelectToggleItem(title: "Services", state: .selectAll, group: "services", selectAllTitle: "Select all", deselectAllTitle: "Deselect all")
        let item2_1 = ServiceItem(title: "Cleaning", subtitle: "$50", isSelected: true, group: "services", value: Service.cleaning)
        let item2_2 = ServiceItem(title: "Oiling", subtitle: "$25", isSelected: false, group: "services", value: Service.oiling)
        let item2_3 = ServiceItem(title: "Waxing", subtitle: "$25", isSelected: false, group: "services", value: Service.waxing)
        let ok = ActionSheetOkButton(title: "Book Servixce")
        let cancel = ActionSheetCancelButton(title: "Cancel")
        return [title, section1, item1_1, item1_2, margin1, section2, item2_1, item2_2, item2_3, ok, cancel]
    }
}

As you can see, this code is much more expressive and makes our code more robust.

Applying custom appearance to item subclasses

If you want to customize the appearance of an item subclass, make sure that your item type overrides cell(for:) and returns a unique cell type, for instance:

class TimeItem: ActionSheetSingleSelectItem {
    
    var time: Time? {
        return value as? Time
    }
    
    open override func cell(for tableView: UITableView) -> ActionSheetItemCell {
        return TimeItemCell(style: cellStyle)
    }
}

class TimeItemCell: ActionSheetSingleSelectItemCell {}

You can now customize your time items by modifying TimeItemCell.appearance():

TimeItemCell.appearance().titleColor = .purple

Adding a menu as a context menu

You can add menus as iOS 13 context menus to any view you like:

menu.addAsContextMenu(to: view) { sheet, item in ...
    print("You selected \(item.title)")
}

You can find more information in this context menu guide.

Context Menus

Sheeeeeeeeet Menus can be added as iOS 13 context menus to any view.

How to present a Menu as a context menu

When you have a Menu instance, you can add it as an iOS 13 context menu to any view:

let item1 = MenuItem(title: "Option 1")
let item2 = MenuItem(title: "Option 2")
let cancel = CancelButton(title: "Cancel")
let menu = Menu(title: "My menu", items: [item1, item2, cancel])
delegate = menu.addAsContextMenu(to: view) { item
    print("You selected \(item.title)")
}

This creates a standard iOS 13 context menu that is applied in the standard way.

Retaining is important

You must retain the delegate, otherwise the context menu will stop working when the delegate is automatically disposed.

To simplify this, you can let view implement ContextMenuDelegateRetainer and use an auto-retaining version of the function:

menu.addAsRetainedContextMenu(to: view, action: ...)

The view will then automatically retain the delegate and release it when it’s disposed.

Unsupported item types

Note that some menu item types can’t be used in context menus. For instance, SelectItems can’t be used, since context menus are designed to be dismisses when a selection is made.

Some item types will be automatically ignored when you convert a menu to a context menu. They are automatically omitted, without any side-effects.

Other item types can’t be automatically ignored, since that would make the menu incomplete. If your menu contains such items, any attempts to present the menu as a context menu will fail with an error.

Presenting a menu as an alert controller

You can present menus as UIAlertControllers:

let delegate = menu.presentAsAlertController(in: self, from: view) { sheet, item in ...
    print("You selected \(item.title)")
}

You can find more information in this alert controller guide.

Alert Controllers

Although Sheeeeeeeeet was created to work around the very limited UIAlertController, Sheeeeeeeeet Menus can still be presented as alert controllers.

How to present a Menu as an UIAlertController

When you have a Menu instance, you can create an UIAlertController with toAlertController(...):

let item1 = MenuItem(title: "Option 1")
let item2 = MenuItem(title: "Option 2")
let cancel = CancelButton(title: "Cancel")
let menu = Menu(title: "My menu", items: [item1, item2, cancel])
let result = menu.toAlertController { item
    print("You selected \(item.title)")
}

However, the easiest way is to present it directly:

menu.presentAsAlertController(in: vc, from: button) { item
    print("You selected \(item.title)")
}

The alert controller will be presented as a regular alert controller.

Unsupported item types

Note that some menu item types can’t be used in alert controllers. For instance, SelectItems can’t be used, since alert controllers are designed to be dismisses when a selection is made.

Some item types will be automatically ignored when you convert a menu to an alert controller. They are automatically omitted, without any side-effects.

Other item types can’t be automatically ignored, since that would make the menu incomplete. If your menu contains such items, any attempts to present the menu as an alert controller will fail with an error.

Download

Custom Action Sheets

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *