Swiftpack.co - Package - elegantchaos/Actions

Test results Latest release swift 5.0 shield swift 5.1 shield swift 5.2 shield swift dev shield Platforms: macOS, iOS, watchOS

Actions

An abstraction for action-handling, for Swift applications.

Stability

Note that the API is in flux currently.

Although I'm using semantic version numbers, I will abuse them until things settle down - so new maintenance releases are likely to contain breaking changes for a while. This is simply to avoid prematurely ending up at version 10.x!

Concepts

Actions are discrete pieces of work that do something. This may be modifying the model, or performing a user interface action, it doesn't really matter.

Actions are registered with the ActionManager, using an identifier. They are then invoked via the ActionManager, using the same identifier.

Actions are decoupled from each other, and from everything that they don't need to perform their specific task.

When an action is performed, it is passed a context. This contains all the information that it needs to perform its action, and is the main mechanism for ensuring that coupling is loose and dynamic.

The context supplied to the action is filled in by items in the responder chain. In this way, it is literally dependent on the user interface context - which window is at the front, which item has focus, and so on. The same action can be invoked in many different situations, as long as something in the responder chain supplies the correct context. Swift's type safety helps here, making it easy to extract the relevant parameters from the context ensuring that they are of the right type.

UI Integration

The Actions module has no dependencies on AppKit/UIKit. It could be used to implement actions for a command line application, or on a non-Apple platform.

The ActionsKit module, on the other hand, builds on top of Actions and integrates it into the responder chain for AppKit or UIKit.

This allows you to bind UI buttons, menus etc to send performAction selector to the responder chain, and have the action manager pick them up, infer the action to execute, and perform is. It also implements some validation support.

Undo support is not there yet, but will be added.

Usage

Setup

Make an ActionManager, attach it to something global (eg your app delegate), and register some actions with it.

If you're going to bind UI items to it, use one of the ActionManagerMac or ActionManagerMobile subclasses, and hook it into the responder chain by calling installResponder.

class Application: NSObject, NSApplicationDelegate {
let actionManager = ActionManagerMac()

func applicationWillFinishLaunching(_ notification: Notification) {
    actionManager.register([
        MyAction(identifier: "MyAction"),
        AnotherAction(identifier: "AnotherAction")
    ])
    actionManager.installResponder()
}

Invocation

Set the action of user interface objects to performAction(_ sender: Any), and the target to the first responder. Set the identifier of the UI item to the identifier of the action you want to invoke.

Alternatively, invoke an action directly with actionManager.perform("MyAction").

Actions

Actions are classes.

To define an action, inherit from Action, and implement perform:

class MyAction: PersonAction {
    override func perform(context: ActionContext) {
        // do stuff here
    }
}

Context

The context passed to perform contains the original sender.

It also contains a dictionary of other information. Items in the responder chain can add items to this dictionary whenever actions are performed, by implementing the ActionContextProvider protocol.

This lets a view controller or window controller pass essential information to actions whilst keeping them fully decoupled.

The action to invoke can be passed explicitly, or parsed out of the identifier of a user interface item.

User interface identifiers can take the form: {prefix.}action{("key": "value", "key2": "value2")}.

The optional prefix, which is discarded, can be any string, and can contain full stops.

This allows you to bind multiple user interface items to the same action, without giving them exactly the same identifier string (Xcode complains if they aren't unique). All of the following identifiers would invoke the MyAction action: MyAction, button.MyAction, menu.MyAction, some.other.thing.MyAction.

If brackets are present after the action, their contents is interpreted as a list of key,value pairs to be added to the context. For example, two buttons might have identifiers to MyAction("color": "red") and MyAction("color": "blue"). Both will invoke the MyAction action, but the value of context["color"] will be set to red or blue respectively.

Currently these arguments are parsed as if they were a JSON dictionary, so both the key and the value need to be quoted. Xcode actually complains about the presence of the " and : characters in identifiers, so a future version may remove this restriction and allow you to specify simply (key: value, key2: value2).

Validation

Actions are often only valid in certain situations - for example when some text is selected - or they want to change their name or visibility depending on the context.

To perform validation, override the validate(context: ActionContext) -> Validation and examine the context that's passed in.

When using the Mac or iOS action managers, validation is invoked automatically for certain user interface actions. In other situations you can invoke it manually as appropriate. The automatic validation is almost always appropriate, but if there's a situation where you want to skip it, you can do so by arranging for the context to contain a true value for the skipValidation.

Action Observers

If your user interface wants to know when certain actions have been performed, this pattern may be useful.

Define a protocol for observers of your action(s). This can contain anything you need.

Implement your protocol in the user interface controllers that want to observe. Also implement the ActionContextProvider protocol, and append the controller to a key that the action(s) will read:


protocol MyActionObserver {
  func myMethod(myArgument: String)
}

extension MyViewController: ActionContextProvider, MyActionObserver {

func provide(context: ActionContext) {
    context.append(key: "MyActionObserver", value: self)
}

In the action, as well as perfoming the actual work, enumerate the observer key. For each observer, call a method from your protocol, passing any arguments or context that is relevant:

class MyAction: PersonAction {
    override func perform(context: ActionContext) {
        // do some stuff here
        
        // notify observers
        context.forEach(key: "MyActionObserver") { (observer: MyActionObserver) in
            observer.myMethod(myArgument: "myValue")
        }
    }
}

Github

link
Stars: 0

Dependencies

Used By

Total: 0

Releases

1.5.1 - 2020-03-18 11:29:12

Updated Logger. Added DecodableWithContext protocol.

1.5.0 - 2020-02-10 13:53:45

We now only support asynchronous actions.

1.4.0 - 2019-07-26 15:17:37

This is a breaking change:

  • refactored the validation code to always return names, and to also return an icon name
  • removed old bool-based validation completely
  • replaced enabled and visible properties with a single state property
  • split ActionsKit out into a separate repo so that it can depend on the Localizations package

1.3.5 - 2019-06-19 12:00:19

Require macOS 10.13 (because dependencies do).

1.3.4 - 2019-06-17 12:15:08

Updated coverage.

1.3.3 - 2019-06-17 11:36:29

Another minor dependency update.

1.3.2 - 2019-06-17 11:07:47

Just updated dependent repo urls to use .git for consistency.

1.3.1 - 2019-06-06 16:25:53

Use Logger 1.3.5.

1.3.0 - 2019-05-22 12:12:13

Added URL coercion support. Removed deprecated parameters. Warn when the same action is registered twice. Assign default identifiers when not explicitly supplied. Added some validation support to ActionManagerMac.

1.2.4 - 2019-03-13 17:10:40

Added rudimentary serialization support.

1.2.3 - 2019-03-11 16:40:44

Allow global notification handlers to be registered with the action manager, so that they can be called for every action performed.

Add a packed method to the action context which can turn an action invocation into a dictionary, for storage or later replay.

1.2.2 - 2019-01-23 18:08:37

Added ability to disable validation via the context.

1.2.1 - 2018-12-18 15:49:24

Improved logging. Fix for bug where only one observer could be registered for a context.

1.2.0 - 2018-12-10 18:10:14

Added validation mechanism that can disable/hide/rename commands.

1.1.0 - 2018-11-23 12:08:23

Adds a notification mechanism (#9). Encapsulates info structure (#10). Adds key/value variant for parameter passing (#15). Moves sender into info.

Resolves #9, #10, #15.

1.0.7 - 2018-11-21 11:21:12

Minor improvements

  • added some more standard keys
  • fixed menu validation
  • improved logging

1.0.6 - 2018-10-19 15:59:12

Added more explicit support for observers to the action context. Added the ability to pass an info dictionary in when performing an action.

1.0.5 - 2018-10-11 15:47:09

Added iOS and Linux support. Refactored UI support for macOS/iOS into a separate ActionsKit module. Improved unit test coverage.

1.0.4 - 2018-09-25 16:37:37

Added DelegatedActions which can delegate perform and validate to another action.

The action to delegate to can be calculated dynamically at runtime, allowing an item to stand in for / forward on to other items in a flexible way.

1.0.3 - 2018-09-07 17:06:03

Added support for item validation.

1.0.2 - 2018-09-07 12:39:31

A few tweaks to the API. Added direct support for appending to, and enumerating over context info items that are lists.

1.0.1 - 2018-09-06 17:10:00

Integrated with Travis. Added tests.

1.0.0 - 2018-09-06 16:44:04

Initial version. Just has basic context gathering and action performing.