KatanaElements 1.0.0

KatanaElements 1.0.0

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布最新发布2018年2月
SPM支持SPM

Bending Spoons维护。








Katana是一个现代Swift框架,用于编写iOS和macOS应用程序,深受ReactRedux的启发,为您的应用程序的各个方面提供结构。

  • 逻辑:应用程序的状态完全由单个可序列化数据结构描述,状态的唯一更改方式是派遣一个操作。一个操作是进行状态转换的意图,并包含所有相关信息来完成这项任务。由于所有更改都是集中的,且按严格顺序发生,因此没有任何细微的竞态条件需要关注。
  • UI:UI是以组件树的形式定义的,这些组件通过道具(即配置数据,例如按钮的背景颜色)和状态(即内部状态数据,例如按钮的突出显示状态)声明性地描述。这种方法让您可以将组件视为独立的、可重用的UI块,因为组件的呈现方式仅取决于组件自身的当前道具和状态。
  • 逻辑 ↔️ UI:UI组件连接到应用程序状态,并且会在每次状态更改时自动更新。您可以通过选择将哪些应用程序状态的部分用于组件道具来控制它们的更改。为了尽可能快速地呈现此过程,仅更新UI的相关部分。
  • 布局:Katana定义了一种简洁的语言(受Plastic的启发),用于描述完全响应式的布局,该布局会优雅地适应每个纵横比或大小,包括字体大小和图像。

自从开始在生产中使用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
}

定义 UI

在 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 以反映 propsstate 的地方。注意,对于常见的属性,如框架、背景颜色等,我们已提供标准 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())
          }
        ]
      }))
  	]
  }
}

将 UI 连接到逻辑

Renderer 负责渲染 UI 树,并在 Store 发生更改时更新它。

您可以从顶层 NodeDescriptionStore 开始创建 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
  }
}

UI 的布局

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)
  }
}

关于 macOS 中的布局的注意事项

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,但不想完全重写它
  • 您想逐渐将您的应用程序迁移到 Katana

渐进式采用无需执行与标准 Katana 使用不同的操作。您只需将您的初始 NodeDescription 渲染到您希望放置 Katana 管理的 UI 的视图中。

假设您在一个视图控制器中,并有一个名为 DescriptionNodeDescription,您可以这样做

// 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 维护。
我们创造了我们自己的技术产品,被全世界成千上万的用户使用和喜爱。
感兴趣? 查看我们