Home

Awesome

Katana UI

Twitter URL Build Status Docs CocoaPods Licence

Katana UI is a modern Swift framework for writing iOS apps, strongly inspired by React and Redux, that gives structure to all the aspects of your app:

Katana UI
🎙Declaratively define your UI
📦Store all your app state in a single place
💂Clearly define what are the actions that can change the state
😎Describe asynchronous actions like HTTP requests
💪Support for middleware
🎩Automatically update the UI when your app state changes
📐Automatically scale your UI to every size and aspect ratio
🐎Easily animate UI changes
📝Gradually migrate your application to Katana

State of the project

We wrote several successful applications using the declarative UI layer that Katana UI provides. We still think that the declarative approach is really a good one when it comes to complex UIs that have to manage several states and transitions. At the same time, we also spent a considerable amunt of time in bridging UIKit's features into Katana UI layer. While in some cases the bridge was easy to implement, in other cases we had to create non trivial code to manage the gap between UIKit and Katana. We felt that being continously in contrast with UIKit really wasn't the way to go and so we decided to put some effort to fix this problem. The result is Tempura. Tempura is a lightweight, UIKit friendly, UI layer that aims to provide a declarative-like approach to UI without being in contrast with UIKit. We love it, and we really encourage you to check it out!

Overview

Defining the logic of your app

The business logic of the application is written using Katana. Please refer to the project's documentation for more information about how to model the state and modify it.

Defining the UI

In Katana you declaratively describe a specific piece of UI providing a NodeDescription. Each NodeDescription will define the component in terms of:

struct CounterScreen: NodeDescription {
	typealias StateType = EmptyState
	typealias PropsType = CounterScreenProps
	typealias NativeView = UIView
	
	var props: PropsType
}

Inside the props you want to specify all the inputs needed to render your NativeView and to feed your children components.

struct CounterScreenProps: NodeDescriptionProps {
  var count: Int = 0
  var frame: CGRect = .zero
  var alpha: CGFloat = 1.0
  var key: String?
}

When it's time to render the component, the method applyPropsToNativeView is called: this is where we need to adjust our nativeView to reflect the props and the state. Note that for common properties like frame, backgroundColor and more we already provide a standard applyPropsToNativeView so we got you covered.

struct CounterScreen: NodeDescription {
  ...
  public static func applyPropsToNativeView(
      props: PropsType,
      state: StateType,
      view: NativeView, ...) {

  	view.frame = props.frame
  	view.alpha = props.alpha
  }
}

NodeDescriptions lets you split the UI into small independent, reusable pieces. That's why it is very common for a NodeDescription to be composed by other NodeDescriptions as children, generating the UI tree. To define child components, implement the method childrenDescriptions.

struct CounterScreen: NodeDescription {
  ...
  public static func childrenDescriptions(
      props: PropsType,
      state: StateType, ...) -> [AnyNodeDescription] {

  	return [
      Label(props: LabelProps.build({ (labelProps) in
          labelProps.key = CounterScreen.Keys.label.rawValue
          labelProps.textAlignment = .center
          labelProps.backgroundColor = .mediumAquamarine
          labelProps.text = NSAttributedString(string: "Count: \(props.count)")
      })),
      Button(props: ButtonProps.build({ (buttonProps) in
        buttonProps.key = CounterScreen.Keys.decrementButton.rawValue
        buttonProps.titles[.normal] = "Decrement"
        buttonProps.backgroundColor = .dogwoodRose
        buttonProps.titleColors = [.highlighted : .red]
        
        buttonProps.touchHandlers = [
          .touchUpInside : {
            dispatch(DecrementCounter())
          }
        ]
      })),
      Button(props: ButtonProps.build({ (buttonProps) in
        buttonProps.key = CounterScreen.Keys.incrementButton.rawValue
        buttonProps.titles[.normal] = "Increment"
        buttonProps.backgroundColor = .japaneseIndigo
        buttonProps.titleColors = [.highlighted : .red]
        
        buttonProps.touchHandlers = [
          .touchUpInside : {
            dispatch(IncrementCounter())
          }
        ]
      }))
  	]
  }
}

Attaching the UI to the Logic

The Renderer is responsible for rendering the UI tree and updating it when the Store changes.

You create a Renderer object starting from the top level NodeDescription and the Store.

renderer = Renderer(rootDescription: counterScreen, store: store)
renderer.render(in: view)

Every time a new app State is available, the Store dispatches an event that is captured by the Renderer and dispatched down to the tree of UI components. If you want a component to receive updates from the Store just declare its NodeDescription as ConnectedNodeDescription and implement the method connect to attach the app Store to the component props.

struct CounterScreen: ConnectedNodeDescription {
  ...
  static func connect(props: inout PropsType, to storeState: StateType) {
  	props.count = storeState.counter
  }
}

Layout of the UI

Katana has its own language (inspired by Plastic) to programmatically define fully responsive layouts that will gracefully scale at every aspect ratio or size, including font sizes and images. If you want to opt in, just implement the PlasticNodeDescription protocol and its layout method where you can define the layout of the children, based on the given referenceSize. The layout system will use the reference size to compute the proper scaling.

struct CounterScreen: ConnectedNodeDescription, PlasticNodeDescription, PlasticReferenceSizeable {
  ...
  static var referenceSize = CGSize(width: 640, height: 960)
  
  static func layout(views: ViewsContainer<CounterScreen.Keys>, props: PropsType, state: StateType) {
    let nativeView = views.nativeView
    
    let label = views[.label]!
    let decrementButton = views[.decrementButton]!
    let incrementButton = views[.incrementButton]!
    label.asHeader(nativeView)
    [label, decrementButton].fill(top: nativeView.top, bottom: nativeView.bottom)
    incrementButton.top = decrementButton.top
    incrementButton.bottom = decrementButton.bottom
    [decrementButton, incrementButton].fill(left: nativeView.left, right: nativeView.right)
  }
}

You can find the complete example here

<table> <tr> <th> <img src="https://raw.githubusercontent.com/BendingSpoons/katana-ui-swift/master/.github/Assets/demo_counter.gif" width="300"/> </th> </tr> </table>

Where to go from here

Give it a shot

pod try KatanaUI

Explore sample projects

<table> <tr> <th> <img src="https://github.com/BendingSpoons/katana-ui-swift/blob/master/.github/Assets/demo_pokeAnimation.gif?raw=true" width="200"/> </th> <th> <img src="https://github.com/BendingSpoons/katana-ui-swift/blob/master/.github/Assets/demo_codingLove.gif?raw=true" width="200"/> </th> <th> <img src="https://github.com/BendingSpoons/katana-ui-swift/blob/master/.github/Assets/demo_minesweeper.gif?raw=true" width="200"/> </th> </tr> <tr> <th> <a href="https://github.com/BendingSpoons/katana-ui-swift/blob/master/Examples/PokeAnimations">Animations Example</a> </th> <th> <a href="https://github.com/BendingSpoons/katana-ui-swift/blob/master/Examples/CodingLove">Table Example</a> </th> <th> <a href="https://github.com/BendingSpoons/katana-ui-swift/blob/master/Examples/Minesweeper">Minesweeper Example</a> </th> </tr> </table>

Installation

Katana UI is available through CocoaPods

Requirements

CocoaPods

CocoaPods is a dependency manager for Cocoa projects. You can install it with the following command:

$ sudo gem install cocoapods

To integrate Katana UI into your Xcode project using CocoaPods you need to create a Podfile.

For iOS platforms, this is the content

use_frameworks!
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.4'

target 'MyApp' do
  pod 'Katana'    
  pod 'KatanaUI'
  pod 'KatanaElements'
end

Now, you just need to run:

$ pod install

Gradual Adoption

You can easily integrate Katana UI in existing applications. This can be very useful in at least two scenarios:

A gradual adoption doesn't require nothing different from the standard Katana UI usage. You just need to render your initial NodeDescription in the view where you want to place the UI managed by Katana.

Assuming you are in a view controller and you have a NodeDescription named Description, you can do something like this:

// get the view where you want to render the UI managed by Katana UI
let view = methodToGetView()
let description = Description(props: Props.build {
	$0.frame = view.frame
})

// here we are not using the store. But you can create it normally
// You should also retain a reference to renderer, in order to don't deallocate all the UI that will be created when the method ends
let renderer = Renderer(rootDescription: description, store: nil)

// render the UI
renderer!.render(in: view)

Get in touch

Special thanks

Contribute

Run the project

In order to run the project, you need xcake. Once you have installed it, go in the Katana UI project root and run xcake make

License

Katana UI is available under the MIT license.

About

Katana has been created by Bending Spoons. We create our own tech products, used and loved by millions all around the world. Interested? Check us out!