Classes

The following classes are available globally.

  • The context in which an action executes. Contains the initial state and contextual logger.

    The context can be passed forward, or a new instance derived with new state, so that e.g. a subsystem can be passed the logger and state information as a single opaque value, without passing forward the entire action request

    See more

    Declaration

    Swift

    public class ActionContext<InputType> where InputType : FlintLoggable
  • A type that tracks metadata about a single action, including its URL Mappings.

    This is used for debug UIs.

    See

    FlintUI.FeatureBrowserFeature
    See more

    Declaration

    Swift

    public class ActionMetadata
  • An action request encapsulates the information required to perform a single action and perform various action auditing.

    This needs class semantics for identity.

    See more

    Declaration

    Swift

    public class ActionRequest<FeatureType, ActionType> : CustomDebugStringConvertible where FeatureType : FeatureDefinition, ActionType : Action
  • The is the internal Flint feature for automatic NSUserActivity publishing and handling.

    This provides actions used internally to publish and handle NSUserActivity for actions that opt-in to this by setting their activityEligibility property.

    See more

    Declaration

    Swift

    public final class ActivitiesFeature : ConditionalFeature
  • This is a builder for creating Metadata instances without requiring a mutable type or ugly initializer permutation.

    See

    ActivityMetadata.build for the function that creates this builder.
    See more

    Declaration

    Swift

    public class ActivityMetadataBuilder
  • This class observes action execution and passes the analytics data to your analytics subsystem for any Actions that support analytics.

    This allows internal or third party frameworks that use Flint to expose functionality that you also track with analytics, without them directly linking to the analytics package.

    To use Analytics reporting, you need to make sure AnalyticsFeature.isEnabled is set to true, and you need to set an implementation of AnalyticsProvider to the AnalyticsFeature.provider property before Flint setup is called. By default this includes just a default console analytics provider.

    AnalyticsFeature.provider = MyGoogleAnalyticsProvider()
    

    See

    AnalyticsProvider for the protocol to conform to, to wire up your actual analytics service such as Mixpanel, Google Analytics or preferably, your own back end.
    See more

    Declaration

    Swift

    public class AnalyticsReporting : ActionDispatchObserver
  • The standard feature availability checker that supports the user toggling features (in User Defaults) that permit this, as well as purchase-based toggling. It caches the results in order to avoid walking the feature graph every time there is a check.

    To customise behaviour of user toggling, implement UserFeatureToggles and pass it in to an instance of this class.

    To customise behaviour of purchase verification, implement PurchaseValidator and pass it in to an instance of this class.

    This class implements the PurchaseRequirement logic to test if they are all met for features that require purchases.

    See more

    Declaration

    Swift

    public class DefaultAvailabilityChecker : AvailabilityChecker
  • The implementation of the system permission checker.

    This registers and verifies the approprite adapters and uses them to check the status of all the permissions required by a feature.

    !!! TODO: Add sanity check for missing Info.plist usage descriptions?

    See more

    Declaration

    Swift

    public class DefaultPermissionChecker : SystemPermissionChecker, CustomDebugStringConvertible
  • The default dispatcher implementation that provides the ability to observe when actions are performed, across sessions.

    This will attempt to detect if the caller is on the queue the action expects, and avoid crashing with a DispatchQueue.sync call when already on that queue. If the current queue is not the action’s queue, it will use a DispatchQueue.sync.

    The goal is that if the app is on the main queue, call perform and the dispatcher than finds the action expects the main queue, that no queue or async thread hops occur. This prevents a slushy UI that is always updating asynchronously, and means the caller does not have to worry about the queue they are on, and nor does the Action.

    However, it is important to note that this mechanism (using setSpecific/getSpecific) will not currently work if the Action’s queue uses a target queue.

    See more

    Declaration

    Swift

    public class DefaultActionDispatcher : ActionDispatcher
  • This is a simple action dispatch observer that will log the start and end of every action execution.

    To add this to aid debugging, just add the following code:

    Flint.dispatcher.add(observer: ActionLoggingDispatchObserver.instance)
    
    See more

    Declaration

    Swift

    public class ActionLoggingDispatchObserver : ActionDispatchObserver
  • An ActionSession is used to group a bunch of Action invocations, ensure they are invoked on the expected queue, and track the Action Stacks that result from performing actions.

    The session used for an action invocation is recorded in Flint timelines and logs to aid in debugging.

    The default ActionSession.main session provided is what your UI will use most of the time. However if your application supports multiple concurrent windows or documents you may wish to create more so that you can see timeline and log events broken down per window or document, e.g. with the session name equal to the document name or a symbolic equivalent of it for privacy. This way you can see what the user is doing in a multi-context environment.

    Furthermore, if your application performs background tasks you should consider creation a session for these.

    The lifetime of an ActionStack can be tracked in logging and analytics and tied to a specific activity session, and is demarcated by the first use of an action from a feature, and the action that indicates termination of the current feature.

    Threading

    A session can be accessed from any queue or thread. The ActionSession.perform method can be called directly or via the StaticActionBinding/VerifiedActionBinding convenience perform methods without knowing whether the queue is correct for the action.

    Actions can select which queue they will be called to perform on, via their queue property. This is always the queue they will execute on, and may be entirely different from the session’s queue.

    This mechanism guarantees that code calling into an ActionSession does not need to care about the queue an Action expects, and Actions do not need to care about the queue they are called on, thus eliminating excessive thread hops (AKA mmm, just DispatchQueue.async it). This reduces slushiness and lag in UIs, and makes it easier to reason about the code.

    The dispatcher will ensure that the Actions are called synchronously on their desired queue, even if that is the same as the current queue. It will also make sure that they call their completion handler on the completion requirement’s callerQueue, without excessive queue hops so that if the caller is already on the correct thread, there is no async dispatch required.

    !!! TODO: Extract protocol for easier testing

    See more

    Declaration

    Swift

    public class ActionSession : CustomDebugStringConvertible
  • An Action Stack is a trail of actions a user has performed from a specific Feature, with sub-stacks created when the user then uses an action from another feature, so each stack can represent a graph of actions broken down by feature.

    Certain actions will Close their stack, e.g. a Close option on a document editing feature. Some stacks may never be closed however, say a DrawingFeature that allows use of many drawing tools. There is no clear end to that except closing the document, an operation on a different feature.

    !!! TODO: Work out what this means for sub-stacks. We want to retain information about what was done in other features, in amongst the current stack’s features, but when the stack closes we don’t want to lose that history if there was not a closing action of the sub stack. Some sub-stacks should be implicitly discarded however - e.g. drawing functions.

    We need reference semantics here because we have parent relationships and navigate the graph.

    !!! TODO: Use LIFOQueue to limit to the number of past items held to avoid blowing/leaking memory over time

    See more

    Declaration

    Swift

    public class ActionStack : CustomDebugStringConvertible
  • This tracker maintains the list of active Action Stacks across all ActionSession(s).

    It is responsible for vending new stacks when required, or existing ones that are not closed.

    See more

    Declaration

    Swift

    public class ActionStackTracker : DebugReportable
  • This is the Flint class, with entry points for application-level convenience functions and metadata.

    Your application must call Flint.quickSetup or Flint.setup at startup to bootstrap the Feature & Action declarations, to set up all the URL mappings and other conventions.

    Failure to do so will usually result in a precondition failure in your app.

    See more

    Declaration

    Swift

    final public class Flint
  • This class acts as a registry of information about your App, for Flint to use.

    Primarily this is used for access to information about the custom URL schemes and universal link domains your app supports.

    Note

    Flint cannot currently extract your supported universal link domains as these are only stored in your entitlements file. The custom URL schemes are listed in your Info.plist so for most cases Flint can extract these.
    See more

    Declaration

    Swift

    final public class FlintAppInfo
  • The set of features provided by Flint itself.

    These are Used to scope and filter logging of Flint itself, and to allow you to disable features of Flint that you do not wish to use.

    See more

    Declaration

    Swift

    public final class FlintFeatures : FeatureGroup
  • A class for managing the debug reporting options of Flint.

    With this class you can generate a debug report ZIP containing all the reports from various subsystems in Flint and also your app.

    Flint’s internal features are automatically registered with DebugReporting, but if you need to add any other data to debug reports you can do so by creating your own object that conforms to DebugReportable and register it here with DebugReporting.add(yourReportableThing).

    See more

    Declaration

    Swift

    public class DebugReporting
  • Metadata describing a single feature.

    The Flint object makes this metadata available for runtime examination of the Features and actions available.

    The FlintUI FeatureBrowserFeature takes advantage of this to provide a hierarchical UI to look at the graph of features and actions defined in the app.

    See more

    Declaration

    Swift

    public class FeatureMetadata : Hashable
  • This is a logger output implementation that captures log events in a buffer of limited length, for use in reporting and UI.

    This will log whatever is coming out of the logging subsystem, so if Focus is enabled, it will capture only items that are in the focus area.

    See

    FocusLogDataAccessFeature which provides realtime access to the data within this logging buffer.
    See more

    Declaration

    Swift

    public class FocusLogging : LoggerOutput
  • The default Focus-aware filtering logger factory.

    This creates contextual loggers that support Focus to restrict logging to specific features at runtime.

    Note that Flint supports log levels per topic path (e.g. by Feature) even without setting one or more focused features.

    This means you can run all your subsystems at info level but turn your app loggic to debug for example.

    See more

    Declaration

    Swift

    public class DefaultLoggerFactory : ContextualLoggerFactory, DebugReportable
  • Logging output to persistent files that can be archived using Flint’s report gathering.

    Note

    Currently does not support maximum log sizes or log file rotation.
    See more

    Declaration

    Swift

    public class FileLoggerOutput : LoggerOutput
  • The contextual logger target implementation used by default, to support the Focus feature of Flint.

    This will use the current FocusSelection to work out whether or not Focus is in effect, and if it is whether or not events should be logged.

    When Focus is active, it will also drop the effective log level to debug so that everything related to your focused areas is output.

    Then focus is not active, a standed log level threshold is applied.

    See more

    Declaration

    Swift

    public class FocusContextualLoggerTarget : ContextualLoggerTarget
  • A single log event.

    This includes high level context information so that logs can identify more accurately what items relate to.

    In the case of Feature based apps, this means you can tell all the log activity that relates to a specific feature just by looking at the logs. Even if it comes from different subsystems.

    See more

    Declaration

    Swift

    public class LogEvent : UniquelyIdentifiable
  • A LoggerOutput implementation that sends log events to the system’s OSLog.

    The app bundle ID plus action session name are used as the subsystem, e.g:

    co.montanafloss.demo.main

    …and the category is set to the topic path (AKA feature + action path):

    AppFeatures/DocumentEditing/#Save

    See more

    Declaration

    Swift

    public class OSLogOutput : LoggerOutput
  • A trivial logger that uses Swift print to stdout. This is not very useful except for debugging without a dependency on another logging framework, as used in Flint demo projects.

    See more

    Declaration

    Swift

    public class PrintLoggerImplementation : LoggerOutput
  • A purchase tracker that allows manually setting of purchase status.

    This can be used standalone as a fake in-memory purchase tracker, or to proxy another purchase tracker implementation so that you can provide overrides at runtime for easier testing.

    On iOS you can use the FlintUI PurchaseBrowserFeature to show a simple UI in your app that will let you view the status of purchases, and if this tracker is used, override the purchase status at runtime for testing.

    See

    see PurchaseBrowserFeature
    See more

    Declaration

    Swift

    public class DebugPurchaseTracker : PurchaseTracker, PurchaseTrackerObserver
  • This type represents information about a product that can be purchased in your app, for use in constraining features to specific products. This is not intended to implement a store client for displaying products and purchasing, but you may extend the types to do this.

    This is used by the purchase conditional feature constraint, allowing you to bind Features to one or more Product, so that if the product is purchased, a group of features can become available.

    Note

    The name and description are primarily used for local debugging. You can use them for your purchase UI in your app but you will need to consider loading the strings for display from a strings bundle using the value of name and description as keys. For StoreKit usage, you need to retrieve the localized price from the App Store. You could do this with a subclass that lazily loads the prices when required.

    Note

    We use class semantics here so that the app can subclass it to include additional properties as required for the purchasing mechanism they use.

    See more

    Declaration

    Swift

    open class Product : Hashable, CustomDebugStringConvertible
  • Use a PurchaseRequirement to express the rules about what purchased products enable your Feature(s).

    You can express complex rules about how your Features are enabled using a graph of requirements. Each Feature can have multiple purchase requirements (combined with AND), but one requirement can match one or all of a list of product IDs, as well as having dependencies on other requirements.

    With this you can express the following kinds of rules:

    • Feature X is available if Product A is purchased
    • Feature X is available if Product A OR Product B OR Product C is purchased
    • Feature X is available if Product A AND Product B AND Product C is purchased
    • Feature X is available if Product A AND (Product B OR Product C) is purchased
    • Feature X is available if (Product A OR Product B) AND ((Product B OR Product C) AND PRODUCT D) is purchased
    • Feature X is available if (Product A OR Product B) AND ((Product B OR Product C) AND PRODUCT D AND PRODUCT E) is purchased

    …and so on. This allows you to map Feature availability to a range of different product pricing strategies and relationships, such as Basic level of subscription plus a Founder IAP that maybe offered to unlock all features in future for a one-off purchase, provided they still have a basic subscription.

    See more

    Declaration

    Swift

    public class PurchaseRequirement : Hashable, Equatable, CustomStringConvertible
  • A basic StoreKit In-App Purchase checker that uses only the payment queue and local storage to cache the list of purchase statuses. It does not validate receipts.

    The local storage is unprotected if the user unlocks the device, and as such may be subject to relatively easy editing by the determined cheapskate user to unlock features.

    Note

    ⚠️⚠️⚠️ Do not use this implementation if you insist on cryprographically verifying purchases.

    Note

    ⚠️⚠️⚠️ It is our view that we should rely on the security of Apple’s platform and not be overly concerned with users performing hacks and workarounds. People that go to the effort of jailbreaking, re-signing apps or applying other patching or data editing mechanisms are unlikely to have paid you any money anyway.

    If this isn’t good enough for you, you will need to add your own app-specific logic to verify this so there isn’t a single point of verification, and to check receipts. You may not want to use Flint for purchase verification at all if it transpires that the Swift call sites for conditional requests are easily circumvented.

    Note

    In-App Purchases APIs are not available on watchOS as of watchOS 5. Any Feature that requires a purchase will not be enabled on watchOS.
    See more

    Declaration

    Swift

    @available(iOS 3, tvOS 9, OSX 10.7, *)
    @objc
    open class StoreKitPurchaseTracker : NSObject, PurchaseTracker
  • A class that is used to create URLs that will invoke App actions.

    Flint Routes support multiple custom app URL schemes and multiple associated domains for deep linking.

    A LinkCreator will only create links for one app scheme or domain - typically apps do not need to generate different kinds of URLs for the same app, but you may need to handle multiple legacy URLs or domains.

    As such, Flint will create a default link creator for the first App URL scheme and Associated Domain that you define in your Info.plist (for app URLs) and the domain you pass when calling Flint.quickSetup.

    This will be used for the automatic link creation for Activities and other system integrations. You can change this behaviour by creating a new LinkCreator for the scheme and domain you prefer, and assign it to Flink.linkCreator.

    You can create your own instances to produce links with specific schemes and domains. Links can only be created for actions that have routes defined in your Feature’s urlMappings.

    See more

    Declaration

    Swift

    public class LinkCreator
  • The action that performs an action associated with a given URL.

    Expected input state type: URL Expected presenter type: PresentationRouter

    This will attempt to resolve the URL against the known URL routes defined on URLMapped features of the app.

    See more

    Declaration

    Swift

    final public class PerformIncomingURLAction : UIAction
  • A URL Pattern matcher that uses Grails-style matching to extract named parameter values from the path and use them like query parameters. The following syntax is supported, per path component, so that macros are not able to span components (i.e. path components cannot contain or match /):

    • $(paramName) — e.g. something$(param1), $(param1)something, something$(param1)something. The text where param1 is in the path is extracted into the query parameters with the key param1
    • * — a wildcard that represents 1 or more any characters, e.g. something*, *something, *
    • ** — a wildcard that matches everything after it in the URL path. It is not valid to have ** anywhere except the final path component
    /store/categories/grindcore --> parameters [:]
    /store/$(category)/grindcore --> parameters ["category":x]
    /store/$(category)/items/$(sku) --> parameters ["category":x, "sku": y]
    /store/$(category)/items/** --> parameters ["category":x] (** matches any suffix)
    /store/$(category)/*/whatever --> parameters ["category":x] (* matches any component, not captured)
    /store/$(category)/*/whatever?var1=a --> parameters ["category":x, "var1":"a"]
    /store/*/whatever?var1=a --> parameters ["var1":"a"]
    /store/*/**?var1=a --> parameters ["var1":"a"]
    /** --> parameters [:]
    
    See more

    Declaration

    Swift

    public class RegexURLPattern : URLPattern
  • A simple representation of the supported URLMapping(s) for actions.

    This is produced by the URLMappingsBuilder, and used to collect all the mappings for a single feature, with a sort of type-erasure for the action type, which is required for action metadata binding elsewhere, so we can show developers the URLs mapped to a given action type

    Declaration

    Swift

    public class URLMappings
  • Builder that creates a URLMappings object, containing all the mappings for a single feature.

    This is used to implement the URL mappings convention of a Feature, which binds schemes, domains and paths to actions. An instance is passed to a closure so that Features can use a DSL-like syntax to declare their mappings.

    The resulting URLMappings object is returned by the build function, using covariant return type inference to select the correct build function provided by the extensions on Feature.

    See more

    Declaration

    Swift

    public class URLMappingsBuilder
  • The default presenter type for Intent Actions, which provides a single function that will pass the INIntentResponse to the Intent Handler’s completion function.

    Note

    This will assert that the respone is the expected type. We use generics here to get close to true type safety, but because of the nature of the code generated by Xcode, we cannot have a truly statically typed presenter — so we check the type of the response at the point of submitting.
    See more

    Declaration

    Swift

    public class IntentResponsePresenter<ResponseType> where ResponseType : FlintIntentResponse
  • An action dispatch observer that will collect a rolling buffer of N action events, and can notify observers when these entries change. Action requests are converted into audit entries that are immutable, so the values of action state and other information are captured at the point of the action occurring, allow you to see changes in the action state over time through the log.

    This is a high level flattened breadcrumb trail of what the user has done in the app, purely in action terms. No other logging is included, so this is suitable for inclusion in crash reports and support requests.

    Use this to capture the history of what the user has done. You can use Flint.quickSetup or manually add this observer with:

    Flint.dispatcher.add(observer: TimelineDispatchObserver(maxEntries: 50))
    

    See

    Flint.quickSetup which will add this dispatcher for you automatically.
    See more

    Declaration

    Swift

    public class Timeline : ActionDispatchObserver, DebugReportable
  • Timeline Entries encapsulate all the lightweight representations of properties related to an action event to be stored in a timeline without any references to the original data. This is to prevent memory usage spiralling out of control while the app is running.

    !!! TODO: Remove @objc and change entry to struct when Swift bug SR-6039/SR-55 is fixed.

    See more

    Declaration

    Swift

    @objc
    public class TimelineEntry : NSObject, UniquelyIdentifiable
  • The Timeline Feature gathers information about the actions the app performs. You can use this to reproduce the steps the user took to arrive at a certain point or crash.

    Entries are stored in a LIFO queue restricted to a maximum number of entries to prevent using every-growing amounts of memory.

    See

    Timeline.snapshot() for access to the data gathers.
    See more

    Declaration

    Swift

    final public class TimelineFeature : ConditionalFeature
  • A type that handles completion callbacks with safety checks and semantics that reduce the risks of callers forgetting to call the completion handler.

    The type is not concurrency safe (see notes in Threading) and it will always call the completion handler synchronously, using the supplied completionQueue if available, or on whatever the current queue thread is if currentQueue is nil.

    To use, define a typealias for this type, with T the type of the completion function’s argument (use a tuple if your completion requires multiple arguments).

    Then make your function that requires a completion handler take an instance of this type instead of the closure type, and make the function expect a return value of the nested Status type:

    protocol MyCoordinator {
      typealias DoSomethingCompletion = CompletionRequirement<Bool>
    
      func doSomething(input: Any, completionRequirement: DoSomethingCompletion) -> DoSomethingCompletion.Status
    }
    

    Now, when calling this function on the protocol, you construct the requirement instance, pass it and verify the result:

    let coordinator: MyCoordinator = ...
    let completion = MyCoordinator.DoSomethingCompletion( { (shouldCancel: Bool, completedAsync: Bool) in
       print("Cancel? \(shouldCancel)")
    })
    
    The block takes one argument of type `T`, in this case a boolean, and a second `Bool` argument that indicates
    if the completion block has been called asynchronously.
    
    // Call the function that requires completion
    let status = coordinator.doSomething(input: x, completionRequirement: completion)
    
    // Make sure one of the valid statuses was returned.
    // This safety test ensures that the completion from the correct completion requirement instance was returned.
    precondition(completion.verify(status))
    
    // If the result does not return true for `isCompletingAsync`, the completion callback will have already been called by now.
    if !status.isCompletingAsync {
        print("Completed synchronously: \(status.value)")
    } else {
        print("Completing asynchronously... see you later")
    }
    

    When implementing such a function requiring a completion handler, you return one of two statuses returned by either the CompletionRequirement.completed(_ arg: T) or CompletionRequirement.willCompleteAsync(). The CompletionRequirement will take care of calling the completion block as appropriate.

    func doSomething(input: Any, completionRequirement: DoSomethingCompletion) -> DoSomethingCompletion.Status {
        return completionRequirement.completedSync(false)
    }
    
    // or for async completion, you retain the result and later call `completed(value)`
    
    func doSomething(input: Any, completionRequirement: DoSomethingCompletion) -> DoSomethingCompletion.Status {
        // Capture the async status
        let result = completionRequirement.willCompleteAsync()
        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
            // Use the retained status to indicate completion later
            result.completed(false)
        }
        return result
    }
    
    ## Threading
    
    A `CompletionRequirement` is not concurrency safe. You must not change any properties or call any methods
    after calling `completedSync` or `willCompleteAsync`. State of the object will not change asynchronously once
    you have called either of these.
    
    See more

    Declaration

    Swift

    public class CompletionRequirement<T>
  • A ProxyCompletionRequirement allows you to provide a completion requirement that adds some custom completion logic to an existing completion instance, and then return a possibly modified result value to the original requirement.

    This mechanism allows your code to not care whether the completion you are proxying is called synchronously or not. Normally you need to know if completion you are wrapping would be called async or not, as you would need to capture the async completion status before defining your completion block so it can call completed on the async result.

    This is a bit nasty in the nuance of the implementation. We may remove this if addProxyCompletionHandler

    See more

    Declaration

    Swift

    public class ProxyCompletionRequirement<T> : CompletionRequirement<T>
  • A dispatch queue that will call a sync block inline if it can tell we are already on that queue, avoiding the problem of having to know if you are on that queue already before calling sync().

    It also supports synchronous execution of the block if on the correct queue already, or flipping that to async if we are not on this queue.

    Note

    This is not safe to use when using Dispatch Queues that use a target queue. The block will execute on the target queue, and getSpecific will not return the correct value
    See more

    Declaration

    Swift

    public class SmartDispatchQueue : Equatable
  • A sequence of uncompressed or compressed ZIP entries.

    You use an Archive to create, read or update ZIP files. To read an existing ZIP file, you have to pass in an existing file URL and AccessMode.read:

    var archiveURL = URL(fileURLWithPath: "/path/file.zip")
    var archive = Archive(url: archiveURL, accessMode: .read)
    

    An Archive is a sequence of entries. You can iterate over an archive using a for-in loop to get access to individual Entry objects:

    for entry in archive {
        print(entry.path)
    }
    

    Each Entry in an Archive is represented by its path. You can use path to retrieve the corresponding Entry from an Archive via subscripting:

    let entry = archive['/path/file.txt']
    

    To create a new Archive, pass in a non-existing file URL and AccessMode.create. To modify an existing Archive use AccessMode.update:

    var archiveURL = URL(fileURLWithPath: "/path/file.zip")
    var archive = Archive(url: archiveURL, accessMode: .update)
    try archive?.addEntry("test.txt", relativeTo: baseURL, compressionMethod: .deflate)
    
    See more

    Declaration

    Swift

    public final class Archive : Sequence