{"id":20838743,"url":"https://github.com/royalicing/shohin","last_synced_at":"2025-05-08T21:29:01.172Z","repository":{"id":72483405,"uuid":"133361353","full_name":"RoyalIcing/Shohin","owner":"RoyalIcing","description":"Pragmatic React/Elm-like components \u0026 state management for iOS","archived":false,"fork":false,"pushed_at":"2021-10-26T04:47:35.000Z","size":105,"stargazers_count":5,"open_issues_count":11,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-05-08T21:28:57.189Z","etag":null,"topics":["cocoa-touch","elm-architecture","functional-programming","swift","uikit"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RoyalIcing.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2018-05-14T13:01:41.000Z","updated_at":"2022-10-24T04:14:59.000Z","dependencies_parsed_at":"2023-02-25T12:30:29.716Z","dependency_job_id":null,"html_url":"https://github.com/RoyalIcing/Shohin","commit_stats":{"total_commits":72,"total_committers":2,"mean_commits":36.0,"dds":0.02777777777777779,"last_synced_commit":"05a93b9deeebf2adeb19f70078ab9cf5da835819"},"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RoyalIcing%2FShohin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RoyalIcing%2FShohin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RoyalIcing%2FShohin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RoyalIcing%2FShohin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RoyalIcing","download_url":"https://codeload.github.com/RoyalIcing/Shohin/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253152953,"owners_count":21862291,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cocoa-touch","elm-architecture","functional-programming","swift","uikit"],"created_at":"2024-11-18T01:11:25.932Z","updated_at":"2025-05-08T21:29:01.120Z","avatar_url":"https://github.com/RoyalIcing.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Shohin [![Build Status](https://travis-ci.org/RoyalIcing/Shohin.svg?branch=master)](https://travis-ci.org/RoyalIcing/Shohin)\n\nPragmatic React/Elm-like components \u0026 state management for iOS.\n\n## Philosophy\n\n- Completely opt-in: mix and match with normal iOS code.\n- Pragmatic: integrates with instead of trying to replace UIKit. Keep using view controllers.\n- Extensible: create your own view elements or commands.\n\n## Usage\n\nFirst, we import the Shohin library. We are going to build a number counter, with buttons to increment and decrement. There will also a random button to choose a random number, and a text field to let the user choose their own number.\n\nWithout any of the interface, what our data model boils down to is a single number, which we can store as an `Int`.\n\nWe declare our data model `CounterModel`. Here we use a `struct` with an `Int` variable that will store our counter.\n\nOur message `CounterMsg` has four possible choices for the different user interactions that will change the model. One to increment by 1, on to decrement by 1, one to completely change the counter to a specified `Int`, and another to change it to a random number.\n\n~~~swift\nimport Shohin\n\nstruct CounterModel {\n  var counter: Int = 0\n}\n\nenum CounterMsg {\n  case increment()\n  case decrement()\n  case setCounter(to: Int)\n  case randomize()\n}\n~~~\n\nLet's connect the model to the message with an update function, which takes a message and makes changes to the model.\n\n~~~swift\nlet randomInt = RandomGenerator(toMessage: CounterMsg.setCounter)\n\nfunc update(message: CounterMsg, model: inout CounterModel) -\u003e Command\u003cCounterMsg\u003e {\n  switch message {\n  case .increment():\n    model.counter += 1\n  case .decrement():\n    model.counter -= 1\n  case let .setCounter(newValue):\n    model.counter = newValue\n  case .randomize():\n    // Returns command to generate a random number\n    return randomInt.generate(min: 0, max: 10)\n  }\n  \n  return [] // No command\n}\n~~~\n\nFor `.randomize()`, we use a random generator here named `randomInt`. This is set up to send `CounterMsg.setCounter(to:)` with the generated random number passed in. It’s similar to a callback.\n\nLet’s make a UI so people can view the model, and make changes to update it. Here we are making labels, fields, and buttons.\n\nWe identify each element that the user interacts with using the `CounterKey` string enum.\n\n~~~swift\nenum CounterKey: String {\n  case counter, increment, decrement, randomize, counterField\n}\n\nfunc render(model: CounterModel) -\u003e [Element\u003cCounterMsg\u003e] {\n  return [\n    label(CounterKey.counter, [\n      .text(\"Counter:\"),\n      .textAlignment(.center),\n    ]),\n    field(CounterKey.counterField, [\n      .text(\"\\(model.counter)\"),\n      .onChange { CounterMsg.setCounter(to: $0.text.flatMap(Int.init) ?? 0) }\n    ]),\n    button(CounterKey.increment, [\n      .title(\"Increment\", for: .normal),\n      .onPress(CounterMsg.increment),\n    ]),\n    button(CounterKey.decrement, [\n      .title(\"Decrement\", for: .normal),\n      .onPress(CounterMsg.decrement),\n      .set(\\.tintColor, to: UIColor.red),\n    ]),\n    button(CounterKey.randomize, [\n      .title(\"Randomize\", for: .normal),\n      .onPress(CounterMsg.randomize),\n    ]),\n  ]\n}\n~~~\n\nWe can use AutoLayout too, making constraints between each UI element, and to the superview’s margins guide.\n\n~~~swift\nfunc layout(model: CounterModel, context: LayoutContext) -\u003e [NSLayoutConstraint] {\n  let margins = context.marginsGuide\n  let counterView = context.view(CounterKey.counter)!\n  let decrementButton = context.view(CounterKey.decrement)!\n  let incrementButton = context.view(CounterKey.increment)!\n  let randomizeButton = context.view(CounterKey.randomize)!\n  return [\n    counterView.centerXAnchor.constraint(equalTo: margins.centerXAnchor),\n    counterView.topAnchor.constraint(equalTo: margins.topAnchor),\n    decrementButton.leadingAnchor.constraint(equalTo: margins.leadingAnchor),\n    decrementButton.bottomAnchor.constraint(equalTo: margins.bottomAnchor),\n    incrementButton.trailingAnchor.constraint(equalTo: margins.trailingAnchor),\n    incrementButton.bottomAnchor.constraint(equalTo: margins.bottomAnchor),\n    randomizeButton.centerXAnchor.constraint(equalTo: margins.centerXAnchor),\n    randomizeButton.bottomAnchor.constraint(equalTo: margins.bottomAnchor),\n  ]\n}\n~~~\n\nNow let's get everything connected and running.\n\n~~~swift\nlet mainView = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))\nmainView.backgroundColor = #colorLiteral(red: 0.239215686917305, green: 0.674509823322296, blue: 0.968627452850342, alpha: 1.0)\n    \n// In a UIViewController, you would write the below in `viewDidLoad()`.\nlet program = Program(view: mainView, model: CounterModel(), initialCommand: [], update: update, render: render, layout: layout)\n~~~\n\nWe now have an interactive app! In summary:\n\n1. You have a **model**, which is presented (**rendered** and **laid out**) to the user as views.\n2. Interactions that the user makes (UI events) produce **messages**, which **update** the model.\n3. These **updates** cause the views to be **rendered** and **laid out** again.\n\n## Glossary\n\n- Model: the stored representation of your app's state. Usually a struct.\n- Message: requests to change the model. Usually an enum.\n- View element: value which represents a view (UIView / UIButton / UILabel etc) and its properties (‘props’)\n- render: function implemented to take the current model and return view elements to be displayed to the user.\n- layout: function implemented to take the current model and a context to the rendered views, and return an array of `NSLayoutConstraint` constraining the rendered view to each other and their containing superview.\n- update: function implemented to take the current model and a message to change the model. You mutate the model, and after your render and layout functions will be called again to update the views.\n- Program: connects all of the above into a running cycle of rendering, layout, respond to user input, and updating the model.\n\n## UIView Docs\n\n### Program\n\n```swift\nclass Program\u003cModel, Msg\u003e {\n  init(\n    view: UIView,\n    model: Model,\n    initialCommand: Command\u003cMsg\u003e = default,\n    update: @escaping (Msg, inout Model) -\u003e Command\u003cMsg\u003e = default,\n    render: @escaping (Model) -\u003e [ViewElement\u003cMsg\u003e] = default,\n    layoutGuideForKey: @escaping (String) -\u003e UILayoutGuide? = default,\n    layout: @escaping (Model, LayoutContext) -\u003e [NSLayoutConstraint] = default\n  )\n\n  var model: Model { get }\n\n  func send(_ message: Msg)\n}\n```\n\n### View Element\n\n```swift\nstruct ViewElement\u003cMsg\u003e {\n  typealias MakeView = (UIView?) -\u003e UIView\n  typealias ViewAndRegisterEventHandler = (UIView, (String, MessageMaker\u003cMsg\u003e, EventHandlingOptions) -\u003e (Any?, Selector)) -\u003e ()\n\n  var key: String { get set }\n  var makeViewIfNeeded: MakeView { get set }\n  var applyToView: ViewAndRegisterEventHandler { get set }\n\n  init(\n    key: String,\n    makeViewIfNeeded: @escaping MakeView,\n    applyToView: @escaping ViewAndRegisterEventHandler\n  )\n}\n```\n\n### Props\n\n```swift\nprotocol ViewProp {\n  associatedtype View : UIView\n\n  static func set\u003cValue\u003e(_ keyPath: ReferenceWritableKeyPath\u003cView, Value\u003e, to value: Value) -\u003e Self\n}\n\nextension ViewProp {\n  static func tag(_ tag: Int) -\u003e Self\n}\n```\n\n#### UILabel\n\n```swift\nenum LabelProp\u003cMsg\u003e : ViewProp {\n  typealias View = UILabel\n\n  case text(String)\n  case textAlignment(NSTextAlignment)\n  case applyChange(ChangeApplier\u003cUILabel\u003e)\n}\n\nfunc label\u003cKey, Msg\u003e(_ key: Key, _ props: [LabelProp\u003cMsg\u003e]) -\u003e ViewElement\u003cMsg\u003e\n```\n\n#### UITextField\n\n```swift\nenum FieldProp\u003cMsg\u003e : ViewProp {\n  typealias View = UITextField\n\n  case text(String)\n  case textAlignment(NSTextAlignment)\n  case placeholder(String?)\n  case keyboardType(UIKeyboardType)\n  case returnKeyType(UIReturnKeyType)\n  case applyChange(ChangeApplier\u003cUITextField\u003e)\n  case on(UIControlEvents, toMessage: ((UITextField, UIEvent) -\u003e Msg)?)\n}\n\nfunc field\u003cKey, Msg\u003e(_ key: Key, _ props: [FieldProp\u003cMsg\u003e]) -\u003e ViewElement\u003cMsg\u003e\n```\n\n#### UIControl\n\n```swift\nenum ControlProp\u003cMsg, Control: UIControl\u003e : ViewProp {\n  typealias View = Control\n\n  case on(UIControlEvents, toMessage: (Control, UIEvent) -\u003e Msg)\n  case applyChange(ChangeApplier\u003cControl\u003e, stage: Int)\n\n  static func set\u003cValue\u003e(_ keyPath: ReferenceWritableKeyPath\u003cControl, Value\u003e, to value: Value, stage: Int) -\u003e ControlProp\n}\n\nfunc control\u003cKey, Msg, Control: UIControl\u003e(_ key: Key, _ props: [ControlProp\u003cMsg, Control\u003e]) -\u003e ViewElement\u003cMsg\u003e\n```\n\n#### UIButton\n\n```swift\nextension ControlProp where Control : UIButton {\n  static func title(_ title: String, for controlState: UIControlState) -\u003e ControlProp\n  static func onPress(_ makeMessage: @escaping () -\u003e Msg) -\u003e ControlProp\n}\n\nfunc button\u003cKey, Msg\u003e(_ key: Key, _ props: [ControlProp\u003cMsg, UIButton\u003e]) -\u003e ViewElement\u003cMsg\u003e\n```\n\n#### UISlider\n\n```swift\nextension ControlProp where Control : UISlider {\n  static func value(_ value: Float) -\u003e ControlProp\n  static func minimumValue(_ value: Float) -\u003e ControlProp\n  static func maximumValue(_ value: Float) -\u003e ControlProp\n  static var isContinuous: ControlProp\n}\n\nfunc slider\u003cKey, Msg\u003e(_ key: Key, _ props: [ControlProp\u003cMsg, UISlider\u003e]) -\u003e ViewElement\u003cMsg\u003e\n```\n\n#### UIStepper\n\n```swift\nextension ControlProp where Control : UIStepper {\n  static func value(_ value: Double) -\u003e ControlProp\n  static func minimumValue(_ value: Double) -\u003e ControlProp\n  static func maximumValue(_ value: Double) -\u003e ControlProp\n  static var isContinuous: ControlProp\n}\n\nfunc stepper\u003cKey, Msg\u003e(_ key: Key, _ props: [ControlProp\u003cMsg, UIStepper\u003e]) -\u003e ViewElement\u003cMsg\u003e\n```\n\n#### UISegmentedControl\n\n```swift\nstruct Segment {\n  enum Content {\n    case title(String)\n    case image(UIImage)\n  }\n\n  var key: String { get set }\n  var content: Content { get set }\n  var enabled = true { get set }\n  var width: CGFloat = 0 { get set }\n}\n\nfunc segment\u003cKey\u003e(_ key: Key, _ content: Segment.Content, enabled: Bool = true, width: CGFloat = 0.0) -\u003e Segment\n\nextension UISegmentedControl {\n  var selectedSegmentKey: String! { get }\n}\n\nenum SegmentedControlProp\u003cMsg\u003e : ViewProp {\n  typealias View = UISegmentedControl\n\n  case selectedKey(String)\n  case segments([Segment])\n  case applyChange(ChangeApplier\u003cUISegmentedControl\u003e)\n  case on(UIControlEvents, toMessage: ((UISegmentedControl, UIEvent) -\u003e Msg)?)\n}\n\nfunc segmentedControl\u003cKey, Msg\u003e(_ key: Key, _ props: [SegmentedControlProp\u003cMsg\u003e]) -\u003e ViewElement\u003cMsg\u003e\n```\n\n#### For custom views\n\nAdd your own `static func` to an extension: `extension CustomViewProp where CustomView : YourViewClass {}` \n\n```swift\nenum CustomViewProp\u003cMsg, CustomView: UIView\u003e : ViewProp {\n  typealias View = CustomView\n\n  case backgroundColor(CGColor?)\n  case applyChange(ChangeApplier\u003cCustomView\u003e)\n}\n\nfunc customView\u003cKey, Msg, CustomView: UIView\u003e(_ key: Key, _ viewClass: CustomView.Type, _ props: [CustomViewProp\u003cMsg, CustomView\u003e]) -\u003e ViewElement\u003cMsg\u003e\n```\n\n### Event handling\n\n```swift\nclass MessageMaker\u003cMsg\u003e {\n  init(event makeMessage: @escaping (UIEvent) -\u003e Msg)\n  init\u003cControl: UIControl\u003e(control makeMessage: @escaping (Control, UIEvent) -\u003e Msg)\n  init()\n}\n\nstruct EventHandlingOptions {\n  var resignFirstResponder: Bool { get set }\n\n  init(\n    resignFirstResponder: Bool = false\n  )\n}\n```\n\n### Layout\n\n```swift\nclass LayoutContext {\n  var marginsGuide: UILayoutGuide { get }\n  var safeAreaGuide: UILayoutGuide { get }\n  var readableContentGuide: UILayoutGuide { get }\n  var view: UIView { get }\n\n  func view\u003cKey\u003e(_ key: Key) -\u003e UIView?\n  func guide\u003cKey\u003e(_ key: Key) -\u003e UILayoutGuide?\n}\n```\n\n### UITableView (alpha: subject to change)\n\n- Model: the data used to render the table\n- CellModel: the data representing a cell\n- Msg: changes sent by rendered elements in the table\n- `class TableAssistant`: used to render cells in a `UITableView`\n\n```swift\npublic class TableAssistant\u003cModel, CellModel, Msg\u003e {\n  public var tableView: UITableView\n  public var model: Model\n\n  public init(tableView: UITableView, initial: Model, update: @escaping (Msg, inout Model) -\u003e ())\n\n  public func registerCells\u003cReuseIdentifier\u003e(reuseIdentifier: ReuseIdentifier, render: @escaping (CellModel) -\u003e [CellProp\u003cMsg\u003e], layout: @escaping (_ cellModel: CellModel, _ context: LayoutContext) -\u003e [NSLayoutConstraint])\n\n  public func cell\u003cReuseIdentifier\u003e(_ reuseIdentifier: ReuseIdentifier, _ cellModel: CellModel) -\u003e UITableViewCell\n}\n```\n\n#### Using with a `UITableViewController` subclass\n\n```swift\nstruct Model {\n  // …\n}\n\nenum CellIdentifier : String {\n  case red\n  case orange\n  case blue\n\n  static let allCases: [CellIdentifier] = [\n    .red,\n    .orange,\n    .blue\n  ]\n}\n\nenum MyMsg {\n  case copyText(String)\n}\n\nprivate struct MyItem {\n  // …\n}\n\nextension CellIdentifier {\n  enum ElementKey : String {\n    case label\n    case button\n  }\n\n  func render(item: MyItem) -\u003e [CellProp\u003cMyMsg\u003e] {\n    switch self {\n    case .red:\n      return [\n        .backgroundColor(UIColor.red),\n      ]\n    case .orange:\n      return [\n        .backgroundColor(UIColor.orange),\n        .content([\n          label(ElementKey.label, [\n            .text(\"Orange\"),\n            .set(\\.textColor, to: black),\n          ]),\n          button(ElementKey.button, [\n            .title(\"Orange\", for: .normal),\n            .set(\\.font, to: UIFont.boldSystemFont(ofSize: UIFont.buttonFontSize)),\n            .onPress { .copyText(\"Orange\") }\n          ])\n        ])\n      ]\n    case .blue:\n      return [\n        .backgroundColor(UIColor.blue),\n        .content([\n          label(ElementKey.label, [\n            .text(\"Blue\"),\n            .set(\\.textColor, to: black),\n          ]),\n          button(ElementKey.button, [\n            .title(\"Blue\", for: .normal),\n            .set(\\.font, to: UIFont.boldSystemFont(ofSize: UIFont.buttonFontSize)),\n            .onPress { .copyText(\"Blue\") }\n          ])\n        ])\n      ]\n    }\n  }\n}\n\nfunc layout(item: MyItem, context: LayoutContext) -\u003e [NSLayoutConstraint] {\n  return [ … ]\n}\n\nfunc update(message: MyMsg, model: inout Model) {\n  switch message {\n  case let .copyText(text):\n    UIPasteboard.general.string = text\n  }\n}\n\nclass MyTableViewController: UITableViewController {\n  private var tableAssistant: TableAssistant\u003cModel, MyItem, MyMsg\u003e!\n\n  override func viewDidLoad() {\n    super.viewDidLoad()\n\n    let tableView = self.tableView!\n    tableView.allowsSelection = false\n    // etc\n\n    self.tableAssistant = TableAssistant\u003cModel, MyItem, MyMsg\u003e(tableView: tableView, initial: Model(), update: update)\n\n    for cellIdentifier in CellIdentifier.allCasess {\n      tableAssistant.registerCells(reuseIdentifier: cellIdentifier, render: cellIdentifier.render, layout: cellIdentifier.layout)\n    }\n    \n    tableView.reloadData()\n  }\n\n  // MARK: Table data source\n\n  override func numberOfSections(in tableView: UITableView) -\u003e Int {\n    // …\n  }\n\n  override func tableView(_ tableView: UITableView, numberOfRowsInSection sectionIndex: Int) -\u003e Int {\n    // …\n  }\n\n  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -\u003e UITableViewCell {\n    let cellIdentifier = // …\n    let item = // …\n    return tableAssistant.cell(cellIdentifier, item)\n  }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froyalicing%2Fshohin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Froyalicing%2Fshohin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froyalicing%2Fshohin/lists"}