alexdrone

Render

UIKit a-là SwiftUI.framework [min deployment target iOS10]
By alexdrone

ios swift swiftui uiview uikit unidirectional-data-flow elm-architecture layout-engine virtual-dom reconciliation

Render


CoreRender is a SwiftUI inspired API for UIKit (that is compatible with iOS 10+ and ObjC).


Introduction

TL;DR

Let's build the classic Counter-Example.


The DSL to define the vdom representation is similiar to SwiftUI.


swift
func makeCounterBodyFragment(context: Context, coordinator: CounterCoordinator) -> OpaqueNodeBuilder {
Component<CounterCoordinator>(context: context) { context, coordinator in
VStackNode {
LabelNode(text: "\(coordinator.count)")
.textColor(.darkText)
.background(.secondarySystemBackground)
.width(Const.size + 8 * CGFloat(coordinator.count))
.height(Const.size)
.margin(Const.margin)
.cornerRadius(Const.cornerRadius)
HStackNode {
ButtonNode()
.text("TAP HERE TO INCREASE COUNT")
.setTarget(coordinator, action: #selector(CounterCoordinator.increase), for: .touchUpInside)
.background(.systemTeal)
.padding(Const.margin * 2)
.cornerRadius(Const.cornerRadius)
}
}
.alignItems(.center)
.matchHostingViewWidth(withMargin: 0)
}
}



Label and Button are just specialized versions of the Node<V: UIView> pure function.
That means you could wrap any UIView subclass in a vdom node. e.g.
```swift


Node(UIScrollView.self) {
Node(UILabel.self).withLayoutSpec { spec in
// This is where you can have all sort of custom view configuration.
}
Node(UISwitch.self)
}


``
The
withLayoutSpec` modifier allows to specify a custom configuration closure for your view.


Coordinators are the only non-transient objects in CoreRender. They yeld the view internal state and
they are able to manually access to the concrete view hierarchy (if one desires to do so).


By calling setNeedsReconcile the vdom is being recomputed and reconciled against the concrete view hiearchy.


```swift
class CounterCoordinator: Coordinator{
var count: UInt = 0


func incrementCounter() {
self.count += 1 // Update the state.
setNeedsReconcile() // Trigger the reconciliation algorithm on the view hiearchy associated to this coordinator.
}
}
```


Finally, Components are yet again transient value types that bind together a body fragment with a
given coordinator.


```swift
class CounterViewCoordinator: UIViewController {
var hostingView: HostingView!
let context = Context()


override func loadView() {
hostingView = HostingView(context: context, with: [.useSafeAreaInsets]) { context in
makeCounterBodyFragment(context: context, coordinator: coordinator)
}
self.view = hostingView
}


override func viewDidLayoutSubviews() {
hostingView.setNeedsLayout()
}
}
```


Components can be nested in the node hierarchy.


```swift


func makeFragment(context: Context) {
Component(context: context) { context, coordinator in
VStackNode {
LabelNode(text: "Foo")
Component(context: context) { context, coordinator in
HStackNode {
LabelNode(text: "Bar")
LabelNode(text: "Baz")
}
}
}
}
}


```


Use it with SwiftUI

Render nodes can be nested inside SwiftUI bodies by using CoreRenderBridgeView:
```swift


struct ContentView: View {
var body: some View {
VStack {
Text("Hello From SwiftUI")
CoreRenderBridgeView { context in
VStackNode {
LabelNode(text: "Hello")
LabelNode(text: "From")
LabelNode(text: "CoreRender")
}
.alignItems(.center)
.background(UIColor.systemGroupedBackground)
.matchHostingViewWidth(withMargin: 0)
}
Text("Back to SwiftUI")
}
}
}


struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}


```


Credits:

Layout engine: