CocoaPods trunk is moving to be read-only. Read more on the blog, there are 14 months to go.
| TestsTested | ✓ |
| LangLanguage | SwiftSwift |
| License | MIT |
| ReleasedLast Release | Feb 2018 |
| SPMSupports SPM | ✗ |
Maintained by Bending Spoons.
Katana is a modern Swift framework for writing iOS and macOS apps, strongly inspired by React and Redux, that gives structure to all the aspects of your app:
We feel that Katana helped us a lot since we started using it in production. At Bending Spoons we use a lot of open source projects ourselves and we wanted to give something back to the community, hoping you will find this useful and possibly contribute.
| Katana | |
|---|---|
| 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 |
Your entire app State is defined in a single struct, all the relevant application information should be placed here.
struct CounterState: State {
var counter: Int = 0
}The app State can only be modified by an Action. An Action represents an event that leads to a change in the State of the app. You define the behaviour of the action implementing the updatedState() method that will return the new app State based on the current app State and the Action itself.
struct IncrementCounter: Action {
func updatedState(currentState: State) -> State {
guard var state = currentState as? CounterState else { fatalError("wrong state type") }
state.counter += 1
return state
}
}The Store contains and manages your entire app State and it is responsible for dispatching Actions and updating the State.
let store = Store<CounterState>()
store.dispatch(IncrementCounter())You can ask the Store to be notified about every change in the app State.
store.addListener() {
// the app state has changed
}In Katana you declaratively describe a specific piece of UI providing a NodeDescription. Each NodeDescription will define the component in terms of:
StateType the internal state of the component (es. highlighted for a button)PropsType the inputs coming from outside the component (es. backgroundColor for a view)NativeView the UIKit/AppKit element associated with the componentstruct 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())
}
]
}))
]
}
}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
}
}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)
}
}Plastic is assuming that the coordinate system has its origin at the upper left corner of the drawing area (like in iOS), so if you want to use plastic on macOS, remember to specify isFlipped = true for all your custom native AppKit views that have children components. All the components we provide are already following this convention.
|
|---|
We wrote a getting started tutorial.
pod try Katana
|
|
|
|---|---|---|
| Animations Example | Table Example | Minesweeper Example |
Katana is available through CocoaPods and Carthage, you can also drop Katana.project into your Xcode project.
iOS 8.4+ / macOS 10.10+
Xcode 8.0+
Swift 3.0+
You can easily integrate Katana 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 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
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)In order to run the project, you need xcake. Once you have installed it, go in the Katana project root and run xcake make
Katana is available under the MIT license.
Katana is maintained by Bending Spoons.
We create our own tech products, used and loved by millions all around the world.
Interested? Check us out!