Latest 0.1.0-alpha1
Homepage https://github.com/runtastic/Matrioska
License MIT
Platforms ios 9.0
Dependencies SnapKit
Authors

Language: Swift
Build Status
Version
DocCov
codecov
License
Platform
Carthage compatible

Matrioska lets you create your layout and define the content of your app in a simple way.

NOTE: Matrioska is under active development, until 1.0.0 APIs might and will change a lot. The project is work in progress, see Roadmap or open issues.

The vision of Matrioska is to let you build and prototype your app easily, reusing views and layouts as well as dynamically define the content of your app.
With Matrioska you can go as far as specifing the content and layout of your views from an external source (e.g. JSON).
With this power you can easily change the structure of your app, do A/B testing, staged rollout or prototype.

To build your UI you can use nested Components. A Component can be 3 different things:

  • View: Any UIViewController that can use AutoLayout to specify its intrinsicContentSize
  • Cluster: Views with children (other Components). A cluster is responsible for laying out its children’s views. Since a cluster is itself a view it can also contain other clusters.
  • Wrapper: A View with only one child (a Component). You can see it as a special cluster or as a special view. It’s responsible for displaying its child’s view.
  • Rule: A Component which visibility is specified by a given Rule.

The goal is to provide a tiny but powerful foundation to build your app on top of.
Matrioska will contain a limited set of standard components and we will consider to add more on a case by case basis.
It’s really easy to extend Matrioska to add new components that fits your needs.

Installation

Using CocoaPods:

use_frameworks!
pod ‘Matrioska’

Using Carthage:

github “runtastic/Matrioska”

Usage

Standard Components

Matrioska defines some standard Components that can be used to create your layout:

id usage config
tabbar ClusterLayout.tabBar(children, meta) TabBarConfig and TabConfig (children)
stack ClusterLayout.stack(children, meta) StackConfig

See the documentation for more informations.

Meta

Every Component may handle additional metadata. The Component’s meta is optional and the Component is responsible for handling it correctly. Metadata can be anything from configuration or additional information, for example a view controller title.

ComponentMeta

Every meta has to conform to ComponentMeta, a simple protocol that provides a keyed (String) subscript.
ComponentMeta provides a default implementation of a subscript that uses reflection (Swift.Mirror) to mirror the object and use its property’s names and values. Objects that conform to this protocol can eventually override this behavior.
ZipMeta, for example, is a simple meta wrapper that aggregates multiple metas together; see its documentation and implementation for more info.
Dictionary also conforms to ComponentMeta, this is a convenient way to provide meta but is especially useful to materialize a ComponentMeta coming from a json/dictionary.

ExpressibleByComponentMeta

When creating a new Component you should document which kind of meta it expects. A good way to do this is to also create an object that represents the Component’s meta (e.g. see StackConfig) and make it conform to ComponentMeta.
ExpressibleByComponentMeta, however, provides some convenience methods that lets you load your components from a json or materialize a meta from a dictionary; that is, it lets you express your meta configuration by any ComponentMeta object.
Other than ComponentMeta’s requirements you also need to provide a init?(meta: ComponentMeta), then you can materialize any compatible meta into your own ExpressibleByComponentMeta.

Example:

public struct MyConfig: ExpressibleByComponentMeta {
    public let title: String

    public init?(meta: ComponentMeta) {
        guard let title = meta["title"] as? String else {
            return nil
        }
        self.title = title
    }
}

After defining MyConfig we can materialize it from other ComponentMetas if possible:

MyConfig.materialize([“title”: “foo”]) // MyConfig(title: "foo")
MyConfig.materialize([“foo”: “foo”]) // nil
MyConfig.materialize(nil) // nil
MyConfig.materialize(anotherMyConfigInstance) // anotherMyConfigInstance

Creating Components

Create custom components:

// Create a cluster by extending an existing implementation
extension UITabBarController {
    convenience init(children: [Component], meta: Any?) {
        self.init(nibName: nil, bundle: nil)
        self.viewControllers = children.flatMap { $0.viewController() }
        // handle meta
    }
}

// Any UIViewController can be used as a View
// we can define a convenience init or just use an inline closure to build the ViewController
class MyViewController: UIViewController {
    init(meta: Any?) {
        super.init(nibName: nil, bundle: nil)
        guard let meta = meta as? [String: Any] else { return }
        self.title = meta["title"] as? String
    }
}

Then create models that can be easily used to create the entire tree of views:

let component = Component.cluster(builder: UITabBarController.init, children: [
    Component.view(builder: MyViewController.init, meta: ["title": "tab1"]),
    Component.view(builder: { _ in UIViewController() }, meta: nil),
    ], meta: nil)

window.rootViewController = component.viewController()

Layout

Views are responsible for defining their intrinsicContentSize using AutoLayout, clusters can decide whether to respect their dimensions or not, both vertical and horizontal or also only one of the two.
To make sure that a Component’s UIViewControllerhas a valid intrinsicContentSize you need to add appropriate constraints to the view. To know more about this read the documentation about “Views with Intrinsic Content Size”.

Rulesets to define Component visibility

Since the visibility of a Component may depend on external data, Matrioska provides rules in order to specify it.

Rules are evaluated in order to resolve the visibility of their Component: when evaluating to true, they return their Component‘s view when asked for their view; otherwise they return nil when evaluating to false.

Rules can also be composed in logical operators:

let rule = Rule.not(rule: Rule.simple(evaluator: { false }))
let component = Component.rule(rule: rule, component: someComponent)
let vc = component.viewController() // Evaluates to true, vc is present

---

let rule = Rule.and(rules: [Rule.simple(evaluator: { false }), Rule.simple(evaluator: { true })])
let component = Component.rule(rule: rule, component: cluster)
let vc = component.viewController() // Evaluates to false, vc is nil

The Rule‘s meta will be their Component‘s meta.

Load Components from JSON

Components can also be loaded from JSON. For this, you are responsible for registering factories (Component builders) that will be used when parsing the JSON structure. In order to register factories, usage of JSONFactory is needed:

let jsonFactory = JSONFactory()

jsonFactory.register(with: "tab_bar", factoryBuilder: { (children, meta) in
    ClusterLayout.tabBar(children: children, meta: meta)
})

jsonFactory.register(with: "navigation", factoryBuilder: { (child, meta) in
    Component.wrapper(builder: { _ in UINavigationController() }, child: child, meta: meta)
})

jsonFactory.register(with: "table_view", factoryBuilder: { (meta) in
    Component.view(builder: { _ in UITableViewController() }, meta: meta)
})

jsonFactory.register(with: "is_male", factoryBuilder: { () in
  return User.isMale
})

jsonFactory.register(with: "is_gold_member", factoryBuilder: { () in
  return User.isGoldMember
})

Whenever you register a new factory you should provide the type key that will match the JSON. Check the [provided JSON schema](/Documentation/JSON schema guide.md) for more details on that.

You can register different factories for View, Wrapper, Cluster and Rule Component types using the JSONFactory. After registration, you can use the factory to get the component out of a JSON:

let component = try jsonFactory.component(from: json)

Components, Metas and Rules should also match the JSON schema that the library provides by default.

For instance, whenever using the built-in components (TabBar or Stack), the meta configuration should meet the documented JSON schema.

Check the [JSON schema guide](/Documentation/JSON schema guide.md) for more information.

Roadmap

  • Deep Linking #5

License

Matrioska is released under the MIT License.
At Runtastic we don’t keep an internal mirror of this repo and all development on Matrioska is done in the open.

Latest podspec

{
    "name": "Matrioska",
    "version": "0.1.0-alpha1",
    "summary": "ud83cudf8e create your layout and define the content of your app in a simple way",
    "description": "The vision of Matrioska is to let you build and prototype your app easily, reusing views and layouts as well as dynamically define the content of your app. With Matrioska you can go as far as specifing the content and layout of your views from an external source (e.g. JSON). With this power you can easily change the structure of your app, do A/B testing, staged rollout or prototype.",
    "homepage": "https://github.com/runtastic/Matrioska",
    "license": {
        "type": "MIT",
        "file": "LICENSE"
    },
    "authors": {
        "Alex Manzella": "[email protected]"
    },
    "source": {
        "git": "https://github.com/runtastic/Matrioska.git",
        "tag": "0.1.0-alpha1"
    },
    "platforms": {
        "ios": "9.0"
    },
    "source_files": "Source/**/*",
    "dependencies": {
        "SnapKit": [
            "~> 3.0"
        ]
    },
    "pushed_with_swift_version": "3.0"
}

Pin It on Pinterest

Share This