ChidoriMenu - An easy way to add menus visually similar to iOS 14's Pull Down and Context Menus but with some added benefits

Related tags

Guides ChidoriMenu
Overview

ChidoriMenu 🐦 ⚡️

An easy way to add popover menus visually similar to the Context Menus and Pull Down Menus iOS uses but with some advantages. (Effectively just a rebuild of Pull Down Menus as a custom view controller to gain some flexibility.)

Chidori Menu

With a very simple API:

let chidoriMenu = ChidoriMenu(menu: someReusedInstanceOfUIMenu, summonPoint: CGPoint(x: 100.0, y: 100.0))
chidoriMenu.delegate = self
present(chidoriMenu, animated: true, completion: nil)

Why?

  • Pull Down menus were introduced in iOS 14, and look great, effectively marrying UIActionController and Context Menus from iOS 13, but are only available on iOS 14. This library works on iOS 13 as well.
  • Even with an iOS 14 only target, the Pull Down Menus API is a little rigid for how I personally would like to use it, where it needs to be paired with a component like a UIButton. Further, if it's not a UIButton or UIBarButtonItem (which have an easy to use, direct menu property), such as an arbitrary UIControl, it seemingly requires subclassing the UIControl and implementing some methods. This doesn't work for me because A) I don't control all of my UIControl subclasses (some are handy ones from libraries) and B) even if I did, I still couldn't present it from say, an arbitrary UITapGestureRecognizer (to my knowledge).

All that to say I wanted a system that didn't care where it came from, or require any special subclassing or delegate method implementation, similar to how UIActionController (in action sheet mode), just presents it from a location on screen, and have it work on iOS 13 as well as a bonus, so I don't have to juggle two systems (UIAlertController on iOS 13, and UIMenu on iOS 14).

Installation

Just drop the Swift files in the Source directory into your project (with the except of ExampleViewController.swift unless you want that). Nothing fancy. 🙂

How to Use

It's meant to basically be dropped in and used right away with an existing UIMenu you have, say for a long-press context menu preview interaction in iOS 13 and higher, so you can use one menu for both.

Sample usage:

override func viewDidLoad() {
    super.viewDidLoad()
    
    let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapped(tapGestureRecognizer:)))
    view.addGestureRecognizer(tapGestureRecognizer)
}

@objc private func tapped(tapGestureRecognizer: UITapGestureRecognizer) {
    let tappedPoint = tapGestureRecognizer.location(in: view)
    
    let chidoriMenu = ChidoriMenu(menu: sampleMenu, summonPoint: tappedPoint)
    chidoriMenu.delegate = self
    present(chidoriMenu, animated: true, completion: nil)
}

private lazy var sampleMenu: UIMenu = {
    var postActions: [UIAction] = []

    let upvoteIdentifier = UIAction.Identifier("upvote")
    let upvoteAction = UIAction(title: "Upvote", image: UIImage(systemName: "arrow.up"), identifier: upvoteIdentifier, handler: upvote(action:))
    postActions.append(upvoteAction)

    let downvoteIdentifier = UIAction.Identifier("downvote")

    let downvoteAction = UIAction(title: "Downvote", image: UIImage(systemName: "arrow.down"), identifier: downvoteIdentifier, handler: downvote(action:))
    postActions.append(downvoteAction)

    let saveIdentifier = UIAction.Identifier("save")
    
    let saveAction = UIAction(title: "Save", image: UIImage(systemName: "bookmark"), identifier: saveIdentifier, handler: save(action:))
    postActions.append(saveAction)
    
    let postMenu = UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: postActions)
    return postMenu
}()

private func save(action: UIAction) {
    print("Save called")
}

Rather than pass a closure into the handler property of UIAction, functions in Swift are first class values so I just pass in the method that it should call.

This leads into the main gotcha, in that the handler isn't accessible after the fact, so this library can't call it automatically (I couldn't find a nice way that didn't feel super fragile or likely to leak memory), so implement the ChidoriDelegate method and call the corresponding action there.

In my app I do this by having a top level dictionary mapping of the identifiers to their corresponding function, that way calling it in the delegate is super simple.

So for instance the upvote action would now look like this, storing the corresponding function:

let upvoteIdentifier = UIAction.Identifier("upvote")
actionMappings[upvoteIdentifier] = upvote(action:) // <-- secret sauce addition
let upvoteAction = UIAction(title: "Upvote", image: UIImage(systemName: "arrow.up"), identifier: upvoteIdentifier, handler: downvote(action:))
postActions.append(upvoteAction)

And then in the delegate method:

var actionMappings: [UIAction.Identifier: UIActionHandler] = [:]

func didSelectAction(_ action: UIAction) {
    actionMappings[action.identifier]?(action)
}

Easy peasy. And I'm pretty dang sure there's no retain cycle issues here, as you're only storing a reference to the function to be called, not actually anything within the function itself.

Gotchas

  • Doesn't support nested menus. It will display everything as if the .displayInline attribute is enabled.
  • Requires the passed UIMenu's children to be either entirely UIActions (for just a normal list of actions) or entirely UIMenus (for a list of actions separated into submenus, again, like .displayInline). Don't mix UIAction and UIMenu into the same array (if you're doing that currently, just wrap the UIActions into a UIMenu before passing).
  • As mentioned in How to Use above, it doesn't just leverage the handler property of the UIAction because iOS doesn't expose the handler method UIAction is created. So rather than duplicate code, separate out your method and call it inside handler (will work for context menus) and then also call it in the corresponding delegate method for this library.
  • Requires iOS 13. iOS 12 would have been cool, but UIMenu requires iOS 13 or higher and I didn't want to have a separate backing model, and I wouldn't be able to use SF Symbols, which are really nice to have for this.

Missing Thing That if You're a Pro I'd Greatly Appreciate Help/Insight Into

Custom view controller transitions are hard. One thing I'm jealous of with Apple's Pull Down Menu implementation that I can't quite lock down with mine is that their view controller presentation is completely interruptable, you can tap outside the menu while it's being presented and it'll cancel the presentation and just return to square one. I cannot figure out how to do this for the life of me. Is it really important? No, not really, the animation is 0.4 seconds long, I doubt people would really notice, but it's bugging me deeply, I love that fluid nature of iOS animations. Here's a GIF showing what I mean (Slow Animations is enabled in iOS Simulator to make it easier to see):

iOS Context Menu showing cancellable nature

This library just uses a standard, non-interactive view controller transition to present the menu, but if anyone can figure out how to transform the current implementation into one that is interruptable, you'd be my favorite iOS developer in the world.

For some more details on this, I wrote on my blog about my trials and tribulations trying to achieve this with interruptibleAnimators, interactive transitions, and more, to no avail:

https://christianselig.com/2021/02/interruptible-view-controller-transitions/

You might also like...
👨‍💻Watch the latest and greatest conference videos on your Mac
👨‍💻Watch the latest and greatest conference videos on your Mac

Conferences.digital is the best way to watch the latest and greatest videos from your favourite developer conferences for free on your Mac. Either sea

SwiftLint - A tool to enforce Swift style and conventions, loosely based on Swift Style Guide.
SwiftLint - A tool to enforce Swift style and conventions, loosely based on Swift Style Guide.

SwiftLint - A tool to enforce Swift style and conventions, loosely based on Swift Style Guide.

A package to help track how often the user opened the app and if they opened it in the current version before.

AppOpenTracker AppOpenTracker provides an observable AppOpenTracker.shared instance that can be used to track the last version that the app was opened

Exemplify a LazyVGrid in SwiftUI in a MVVM pattern with Mock Data and images in assets.
Exemplify a LazyVGrid in SwiftUI in a MVVM pattern with Mock Data and images in assets.

SwiftUI-LazyVGrid Exemplify a LazyVGrid in SwiftUI in a MVVM pattern with Mock Data and images in assets. Screenshots Samples IP & Credits All those b

This little app aims to help teach me how to implement more then one API in one single application in a reusable and properly structured manner.

LilAPI App News & Weather This little API app combines two of Jordan Singers Lil Software API's into one app. The goal with this app was to learn how

This repo shows how to set up and use GitHub Actions as a CI for Swift Packages
This repo shows how to set up and use GitHub Actions as a CI for Swift Packages

SwiftPackageWithGithubActionsAsCI This repo shows how to set up and use GitHub Actions as a CI for Swift Packages. Available environments on GitHib Li

A simple solution to decrease build time and more cleaner codebase

Swift Optional Optimizer A simple Protocol Oriented solution to decrease build time and more cleaner code base. Are you tired of using ?? in your code

SpaceX rocket listing app using RxSwift and CLEAN Architecture with MVVM
SpaceX rocket listing app using RxSwift and CLEAN Architecture with MVVM

Jibble SpaceX rocket listing app using RxSwift and CLEAN Architecture with MVVM Demo Features Reactive Bindings URL / JSON Parameter Encoding Filter Y

A trivial app for storing and viewing famous quotes

Paraphrase A trivial app for storing and viewing famous quotes This app was specifically designed to accompany a tutorial series about modern app infr

Comments
  • Add Cancellable Interaction for Presentation Animation

    Add Cancellable Interaction for Presentation Animation

    Hey @christianselig!

    As promised, I had a bit of a play with making the opening presentation animation and I got it working! :D

    Here's the things I changed:

    • I modified ChidoriAnimationController to also conform to the interactive controller protocol, and modified ChidoriMenu to retain and provide the same instance to both animation and interactive delegates.
    • I included a public cancel method that retains the context of the animation to allow cancellations, and also stops, and then restarts the "opening" animation in reverse.
    • When a presentation starts, it is treated as interactive from the start, meaning touches automatically work by default.
    • When the user taps the dimming view, this now forwards the tap through a delegate pattern to ChidoriMenu
    • Chidori menu then calls the cancellation method, but if the animation has already completed, it also calls dismiss to dismiss it as normal.

    I've also included a small Xcode project in order to make it super quick to test this out. I'd also recommend adding CocoaPods and Swift Package Manager support (or if you'd like, I can do that).

    Please try it out, and let me know what you think. There might be some better ways to streamline this logic and code organization, but this should hopefully be a good start.

    All the best!

    opened by TimOliver 5
  • Basic SPM support

    Basic SPM support

    Hey @christianselig! I noticed this project didn't allow for any easy way of installation (except manually copying it over) so I threw together a quick Package.swift and fixed a deprecation warning.

    Don't hesitate to leave any comments or requests before merging this PR, I'm going to try to respond as soon as I can.

    opened by PatrikTheDev 4
  • Make the interruptible transition

    Make the interruptible transition

    Example: https://github.com/MainasuK/ChidoriMenu/tree/feature/interruptible

    I'm not sure that's what you want.

    Depends on the GIF in the README. The iOS system UIMenu:

    1. Tap button. The menu presenting with animation
    2. Tap overlay. The menu reverses the transition to dismissing.
    3. Tap the button again. The menu cancels dismissing and presenting.

    So there must be a reuse or diff mechanism to using the same transitioning. Then just toggle the animator. Finally, call the transition context callback depends on the animator's position end or not. Because the interruptible transition needs marked finish when it's completed.

    I toggle the animator when tapping the overlay. That still needs some work to make that feels like the UIMenu.

    Demo: https://user-images.githubusercontent.com/7940186/119816568-6e477400-bf1f-11eb-9b61-5bbcf5c551f8.mp4

    opened by MainasuK 1
Owner
Christian Selig
Christian Selig
A simple menubar app can give you quick access to some macOS functions

OneClick This simple menubar app can give you quick access to some macOS functio

mik3 32 Dec 19, 2022
A blog project where you can write your articles, upload photos, categorize them, and add them to your favorites

A blog project where you can write your articles, upload photos, categorize them, and add them to your favorites. The aim of the project is to learn the use of Core Data.

Cem 7 Mar 21, 2022
This to learn such as : Add Target , NSNotification Center Send/Get Data , Observer Override , resize Data By Byte , UIImagePicker Delegate , UIAlert Handle , Top ViewController , Get pickerController

Technicalisto How to Create UIButton Class to Pick Data Image Purpose Learn this topics With exact Task Add Target NSNotification Center Send/Get Data

Aya Baghdadi 1 Feb 20, 2022
Easy usage SFSymbols. If symbol not available, compiler will show warning.

SFSymbols Wrapper of SFSymbols. You choose the icon and what style to draw it in. You can specify the font with which to draw the icon. If the symbol

Ivan Vorobei 99 Jan 2, 2023
iOS native app demo with Xcode and Swift using MVVM architecture and Apple's Combine framework for functional reactive programming, all with UIKit

iOS (Swift 5): MVVM test with Combine and UIKit MVVM and functional reactive programming (FRP) are very used in iOS development inside companies. Here

Koussaïla BEN MAMAR 2 Dec 31, 2021
Fineride - An iOS application for managing one's vehicles and their associated maintenance

FineRide An iOS application for managing one's vehicles and their associated mai

Robert Tănase 0 Feb 13, 2022
MMVMi: A Validation Model for MVC and MVVM Design Patterns in iOS Applications

MMVMi MMVMi: A Validation Model for MVC and MVVM Design Patterns in iOS Applications Motivation Design patterns have gained popularity as they provide

Mariam AlJamea 11 Aug 19, 2022
This is an iOS Safari Extension Sample that adds a "Develop menu" to Safari on iOS to allow you to analyze websites.

Develop Menu for Mobile Safari This is an iOS Safari Extension that adds a "Develop menu" to Safari on iOS to allow you to analyze websites. This is a

Watanabe Toshinori 1 Dec 7, 2022
Explanations and samples about the Swift programming language

About Swift Contents Explanations and samples about: Swift Programming Language Swift Standard Library Target audience Developers familiar with object

Nicola Lancellotti - About 74 Dec 29, 2022