Katana是一个现代Swift框架,用于编写iOS和macOS应用程序,深受React和Redux的启发,为您的应用程序的各个方面提供结构。
自从开始在生产中使用Katana以来,我们感觉它帮了我们很多。在Bending Spoons,我们使用了很多开源项目,并希望回馈社区,希望您会认为这很有用,并可能做出贡献。
Katana | |
---|---|
声明性地定义您的UI | |
将所有应用程序状态存储在单个位置 | |
明确定义可以更改状态的动作 | |
描述类似于HTTP请求的异步动作 | |
支持中间件 | |
当您的应用程序状态更改时,自动更新UI | |
自动将UI调整到每个大小和纵横比 | |
轻松动画UI更改 | |
逐步将应用程序迁移到Katana |
您的整个应用程序状态State
在单个结构体中定义,所有相关应用程序信息都应放在这里。
struct CounterState: State {
var counter: Int = 0
}
应用程序 State
只能通过 Action
来修改。 Action
代表一个导致应用程序 State
发生变化的事件。您定义动作的行为,实现 updatedState()
方法,该方法将根据当前应用程序的 State
和动作本身来返回新的应用程序 State
。
struct IncrementCounter: Action {
func updatedState(currentState: State) -> State {
guard var state = currentState as? CounterState else { fatalError("wrong state type") }
state.counter += 1
return state
}
}
Store
包含并管理您整个应用程序的 State
,并且负责分发 Actions
和更新 State
。
let store = Store<CounterState>()
store.dispatch(IncrementCounter())
您可以让 Store
通知您关于应用程序 State
的每一次更改。
store.addListener() {
// the app state has changed
}
在 Katana 中,您通过提供 NodeDescription
来声明性地描述特定的 UI 部分。每个 NodeDescription
都将定义组件,从以下方面考虑:
StateType
组件的内部状态(例如,按钮的高亮显示)PropsType
来自组件外部的输入(例如,视图的背景颜色)NativeView
与组件关联的 UIKit/AppKit 元素struct CounterScreen: NodeDescription {
typealias StateType = EmptyState
typealias PropsType = CounterScreenProps
typealias NativeView = UIView
var props: PropsType
}
在 props
中,您需要指定渲染您的 NativeView
和 feeding 子组件所需的所有输入。
struct CounterScreenProps: NodeDescriptionProps {
var count: Int = 0
var frame: CGRect = .zero
var alpha: CGFloat = 1.0
var key: String?
}
当需要渲染组件时,将调用 applyPropsToNativeView
方法:这就是我们调整 nativeView 以反映 props
和 state
的地方。注意,对于常见的属性,如框架、背景颜色等,我们已提供标准 applyPropsToNativeView,所以我们为您提供了解决方案。
struct CounterScreen: NodeDescription {
...
public static func applyPropsToNativeView(props: PropsType,
state: StateType,
view: NativeView, ...) {
view.frame = props.frame
view.alpha = props.alpha
}
}
NodeDescriptions
允许您将 UI 分割成小的独立、可重用的部分。这就是为什么 NodeDescription
通常会由其他 NodeDescription
(作为子元素)组成,生成 UI 树。要定义子组件,实现 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())
}
]
}))
]
}
}
Renderer
负责渲染 UI 树,并在 Store
发生更改时更新它。
您可以从顶层 NodeDescription
和 Store
开始创建 Renderer
对象。
renderer = Renderer(rootDescription: counterScreen, store: store)
renderer.render(in: view)
每当有新的应用程序 State
可用,Store
就会分发一个事件,该事件会被 Renderer
捕获,并向下分发到 UI 组件树。
如果想让组件从 Store
接收更新,只需将其 NodeDescription
声明为 ConnectedNodeDescription
并实现 connect
方法来连接应用程序 Store
到组件的 props
。
struct CounterScreen: ConnectedNodeDescription {
...
static func connect(props: inout PropsType, to storeState: StateType) {
props.count = storeState.counter
}
}
Katana 有自己的语言(灵感来自于 Plastic),可以以编程方式定义完全响应式的布局,该布局将在每个纵横比或大小下优雅地缩放,包括字体大小和图片。
如果想要启用它,只需实现 PlasticNodeDescription
协议及其 layout
方法,您可以在此方法中定义基于给定 referenceSize
的子元素的布局。布局系统将使用参考大小来计算适当的缩放。
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 假设坐标系的原点位于绘图区域的左上角(类似于 iOS),因此如果您想在 macOS 上使用 Plastic,请记住为所有具有子组件的自定义 native AppKit 视图指定 isFlipped = true
。我们提供的所有组件都遵循此约定。
![]() |
---|
我们编写了一个 入门教程。
pod try Katana
![]() |
![]() |
![]() |
---|---|---|
动画示例 | 表示例 | 扫雷示例 |
Katana 可通过 CocoaPods 和 Carthage 获取,您还可以将 Katana.project
放入您的 Xcode 项目中。
iOS 8.4+ / macOS 10.10+
Xcode 8.0+
Swift 3.0+
您可以轻松地将 Katana 集成到现有应用中。这至少在以下两种情况下非常有用
渐进式采用无需执行与标准 Katana 使用不同的操作。您只需将您的初始 NodeDescription
渲染到您希望放置 Katana 管理的 UI 的视图中。
假设您在一个视图控制器中,并有一个名为 Description
的 NodeDescription
,您可以这样做
// 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)
为了运行项目,您需要 xcake。一次安装后,进入 Katana 项目的根目录并运行 xcake make
Katana 可在 MIT 许可证 下使用。
Katana 由 Bending Spoons 维护。
我们创造了我们自己的技术产品,被全世界成千上万的用户使用和喜爱。
感兴趣? 查看我们!