Swiftpack.co - GoodHatsLLC/ScopeKit as Swift Package

Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
See all packages published by GoodHatsLLC.
GoodHatsLLC/ScopeKit v1.0.0
ScopeKit scaffolds reactive applications, simplifies managing Combine subscriptions, and makes [weak self] a thing of the past.
⭐️ 2
πŸ•“ 6 days ago
iOS macOS
.package(url: "https://github.com/GoodHatsLLC/ScopeKit.git", from: "v1.0.0")

ScopeKit

ScopeKit is a Swift library for managing the lifecycle of Combine subscriptions.

Purpose

Managing application state is hard. Combine is a great way to do it, but it has a steep learning curve and even then leaves error-prone and rote management to be done manually.

ScopeKit allows you to build out a tree of 'scopes' as a scaffold within your app. Scopes own subscriptions, managing their lifecycles automatically and according to simple and predictable rules. [weak self] becomes a thing of the past.

Concepts

ScopeKit's core components are the Scope and Behavior superclasses. Both have the following lifecycle, and can only perform work while active.

Activity Lifecycle

 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  willAttach   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” willActivate β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚  Detached  │──────────────▢│  Attached  │─────────────▢│   Active   β”‚
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β–²                         β”‚    β–²                        β”‚
        β”‚                         β”‚    β”‚                        β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                didDetach                      didDeactivate

You can create a Behavior or Scope by inheriting from their respective superclasses. In the following example any subscription created in willActivate and stored in the provided cancellables will be cancelled when MyBehavior is no longer active. It is restarted if the Behavior once again becomes active.

Scopes have the exact same lifecycle as Behaviors.

final class MyBehavior: Behavior {

    override func willAttach() {}

    override func willActivate(cancellables: inout Set<AnyCancellable>) {
        /*
         myPublisher
            .sink {
                // Work that is scoped to the behavior's active state
            }
            .store(in: &cancellables)
         */
    }

    override func didDeactivate() {}

    override func didDetach() {}

}

Scopes & Behaviors

Scopes differ from Behaviors in that they can contain other Scopes or Behaviors. A Behavior is node of work. A Scope can be a tree.

Scopes and Behaviors can be attached to host-scopes using their attach(to:) methods and detached using detach(). (You can also use the host's host(_:) and evict(_:) respectively, passing the scope to be hosted.)

A Scope can also bind arbitrary subscriptions to its active lifecycle via a CancellableReceiver vended on whileActiveReceiver.

Attachment and activity

Scopes and Behaviors are active when they are attached to a host-Scope which is also active. When their host-Scope is not .active they are .attached and if they have no host-scope they are .detached.

No host Inactive host Active Host
.detached .attached .active
not working not working doing work

The only exception to this rule is the RootScope class, which is always .active and can not have a host.

As such it's possible to build out a tree with a RootScope at the root, Scopes as intermediate nodes, and both Scopes and Behaviors as leaf nodes. Anything attached to this tree has is .active and doing work.

Using lifecycle event

The lifecycle events, willAttach(), willActivate(cancellables:), didDeativate(), and didDetach() should be overriden in subclasses and are called by the library. They shouldn't be called directly. Subclasses do not have to call the various super methods.

willActivate(cancellables:)

willActivate(cancellables:) is the primary lifecycle event. Often no other override is needed as cancel() is called on the cancellables by the library.

willAttach()

willAttach() may be useful if there are linkages between the work the Scopes are doing that don't need to be torn down on deactivation, and can wait until full detachment. For example, if a host Scope that often toggles between activity and inactivity owns a UIViewController a sub-Scope could attach a contained ViewController in willAttach() once, rather than repeatedly in willActivate(cancellables:).

willDetach()

willDetach() exists to allow the subclass to cleaning up any state it set up in willAttach().

willDeactivate()

willDeactivate() is unnecessary as long as the state setup and work started in willActivate(cancellables:) can be attached to the passed cancellables. i.e. as long as it is a Combine subscription. However, if imperative state setup is done in willActivate(cancellables:) it can be torn down here.

Other Constructs

In addition to Scope, Behavior, ScopeKit contains:

RootScope

A non-subclassable, always active, root for a Scope tree.

let leafScope = MyLeafScope()
// leafScope is .detached

let middleScope = MyMiddleScope()
leafScope.attach(to: middleScope)
// leafScope is now .attached β€” but not .active

let rootScope = RootScope()
middleScope.attach(to: rootScope)
// leafScope is now .active and has started its work.

AnonymousBehavior

A convenience Behavior which isn't subclassed but whose willActivate(cancellables:) behavior is passed in in the initializer.

AnonymousBehavior { cancellables in
    somePublisher
        .sink { value in
            print(value)
        }
        .store(in: cancellables)
}

CancellableReceiver

A utility on Scope which stores cancellables to bind subscriptions to its current activity lifecycle.

somePublisher
    .sink { _ in
        print("this print can only happen as long as myScope is .active.")
    }
    .store(in: myScope.whileActiveReceiver)

Usage

  1. Add ScopeKit to your SPM dependencies. https://github.com/GoodHatsLLC/ScopeKit.git
  2. import ScopeKit
  3. Build out a tree of Scopes and Behaviors under your RootScope :)

Example App

This repo contains an example app.

The Xcode projects for the example app are built using Tuist.

cd ExampleApp
tuist dependencies fetch # fetches the in-repo ScopeKit SPM package
tuist generate # generate a project and workspace
open ExampleApp.xcworkspace

State of Project

ScopeKit is new, but should now have a stable API. It's reasonably complicated and is commensurately reasonably tested.

Take it for a spin! Comments/Questions/Contributions are super welcome.

GitHub

link
Stars: 2
Last commit: 5 days ago
jonrohan Something's broken? Yell at me @ptrpavlik. Praise and feedback (and money) is also welcome.

Release Notes

6 days ago

v1.0.0 stabilizes the core API. πŸŽ‰

Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics