Skip to content

mislavjavor/Kandinsky

Repository files navigation

kandinsky

A Swift Layout DSL

Build status Platform iOS Swift 3 compatible CocoaPods compatible License: MIT

Author: Mislav Javor.

Looking for contributors

This project is currently a one man operation. I've dedicated a large chuck of my free time to this, and would be immensely grateful for any help.
Thank you for contributing ❤️

Why Kandinsky

  • Storyboards are hard to maintain, obfuscate functionality, hard to reuse and almost impossible to merge.
  • Writing in code is extremely verbose, time consuming and lacks a high level overview of the layout

Kandinsky combines the expressiveness of storyboards with the power of Swift language.

  • Swift Powered DSL
  • Easy to write and read
  • Modular (build reusable components)
  • Reactive (RxSwift/ReactiveCocoa) friendly
  • Interactive prototyping using playgrounds
  • Easy to merge
  • Turing complete (for loops, if statemets, protocols, interitance etc...) layout code

Example

If we write this:

UIView.set {
     $0.id = "root"
     $0.view.backgroundColor = .lightGray }.add { r in

        UIButton.set {
            $0.id = "myButton"
            $0.view.setTitle(buttonTitle, for: .normal)
            $0.view.setTitleColor(.black, for: .normal)
            $0.centerInParent()
        }/r
}

We get this:

preview

Easily add new elements to your layout. Use playgrounds for live preview

Basic example

Apply behaviour to your layout while you're writing it

Interactivity

Quickly iterate over multiple versions of your layout. Use playground to visualise

Quick Edit

Requirements

  • iOS 9.0+
  • Xcode 8.0+

Getting Started

CocoaPods

CocoaPods is a dependency manager for Cocoa projects.

To install Kandinsky, simply add the following line to your Podfile:

pod 'Kandinsky', '~> 1.0.1'

Carthage

Carthage support coming soon

Basic layout

First make sure to import Kandinsky.

The syntax for the DSL is really simple and intuitive. After you've imported Kandinsky, any class inheriting from UIView (e.g. UIButton, UILabel) will have a set method exposed like so:

UIView.set { (param: Kandinsky<UIView>) -> Void in
    /*
    The ID of the view, which can later be used for styling
    and querying
    */
    param.id = "<id>"

    /*
    The `view` property is the instance of the view which
    is being created (e.g. if you're creating a UIButton the `view`
    property would be a UIButton)
    */
    param.view.backgroundColor = .red
}

This takes care of view creation and customization. The next step is to add subviews. We can set this by calling the add method (note: unlike set, add is not static and must be called after the set block)

UIView.set {
    $0.id = "root"
    $0.view.backgroundColor = .black }.add { r in // calling the `add` method, `r` is placeholder for `root`

        UIButton.set {
            $0.id = "demoButton"
            $0.view.setTitle("Push me!", for: .normal)

            /*
            The framework exposes many methods for adding constraints
            like `centerInParent`, `alignParentLeading`, `under(id:)`,
            `toLeftOf(:)`. You can also easily create your own constraint
            helper methods
            */
            $0.centerInParent()


        }/r // The `/` operator adds the left side item to the right side item
            // in this case it means it adds the Kandinsky<UIButton> to the `r`
            // variable which we declared above and which is an instance of
            // Kandinsky<UIView>. The `/` operator can add any two elements
            // of type `Canvas to one another`
}

Here is a simple example:

let layout =
UIView.set {
    $0.view.backgroundColor = .white }.add { r in

        UILabel.set {
            $0.id = "titleLabel"
            $0.view.text = "Hello world"
            $0.fontSize = 30 // fontSize is a helper function
            $0.centerInParent()
        }/r

        UIButton.set {
            $0.view.setTitle("Push me!", for: .normal)
            $0.view.setTitleColor(.blue, for: .normal)
            $0.under("titleLabel")
            $0.centerHorizontallyInParent()
        }/r

}

This produces a view that looks like this:

Simple example

Implementing the layout

In order to use your layout, simply make your UIViewController implement the Controller protocol. This means adding the didRender(ViewHolder:root:) method to your UIViewController.

Then in the loadView method of your UIViewController, call the setContentView function and pass the instance of your layout

The didRender method will be called after all of the views have been added and constraints set.

You can use it to extract views from the ViewHolder by using the

let myView = views["<view_id>"] as? UIButton // cast to your specific view
class DemoVC: UIViewController, Controller {

    var views: ViewHolder = [:]

    override func loadView() {
        super.loadView()
        setContentView(with: layout)
    }

    func didRender(views: ViewHolder, root: AnyCanvas) {
        self.views = views
        let button = views["pressMeButton"] as? UIButton
        button?.addTarget(self, action: #selector(didTouchButton), for: .touchUpInside)
    }

    func didTouchButton() {
        let title = views["titleLabel"] as? UILabel
        title?.text = "Pressed the button"
        PlaygroundHelper.alert(over: self, message: "Pressed the button")
    }
}

Note - setContentView only sets the view property of the UIViewController and calls the didRender method. You can call it at any time, but it's recommended to call it in the loadView method

Getting the view

If you don't want to inherit the Controller and just want the view from your canvas, you can do it like this:

let view = CanvasRenderer.render(demoLayout)

Extending the framework

This framework is build by following the latest and greatest in the protocol oriented world of Swift. If you wish to add additional functionality, you only need to extend the Canvas protocol

extension Canvas {

    func alignParentLeadingAndTrailing(offset: Int) {
        // If you're working with constraints - you must append your code
        // to the `deferAfterRender` array. Otherwise your app will fail
        deferToAfterRender.append({ views in
            self.view.snp.makeConstraints { make in
                make.leading.equalToSuperview().offset(offset)
                make.trailing.equalToSuperview().offset(-offset)
            }
        })
    }
}

And after you've done that you can call it:

...
UIButton.set {
    ...
    $0.alignParentLeadingAndTrailing(offset: 20)
    ...
}
...

You can also be more specific:

extension Canvas where UIKitRepresentation == UITableView {

    func setDelegateAndDataSource<T>(item: T)
        where T: UITableViewDelegate, T: UITableViewDataSource {

            self.view.delegate = item
            self.view.dataSource = item
    }

}


extension Canvas where UIKitRepresentation: UILabel {

    func setTextToLoremIpsum() {
        self.view.text = "Lorem ipsum dolor sit..." // ...
    }

}

And then those properties will only appear on those types of Canvases

UITableView.set {
    $0.setDelegateAndDataSource(item: delegate)
}

UILabel.set {
    $0.setTextToLoremIpsum()
}

Getting involved

  • If you want to contribute please feel free to submit pull requests.
  • If you have a feature request please open an issue.
  • If you found a bug or need help please check older issues, FAQ and threads on StackOverflow (Tag 'Kandinsky') before submitting an issue..

Before contribute check the CONTRIBUTING file for more info.

Examples

Follow these 3 steps to run Example project:

  • Clone Kandinsky repository
  • Open Kandinsky.xcworkspace
  • Run the Example project

OR

  • Open the Example/Playground and play around with live-preview

Author

Change Log

This can be found in the CHANGELOG.md file.