Intro
- Lightweight & Predictable (Rx dep only)
- You can easy to separate presentation logic from business logic.
- KnotState will make your presentation logic as reusability.
- Support a disposeBag (Just inherit knotable, you don't needs make a disposeBag property :) )
- Efficient updating node layout with state stream.
Quick Example
Node
class Node: ASDisplayNode & Knotable {
struct State: KnotState {
var title: String
var subTitle: String
static func defaultState() -> State {
return .init(title: "-", subTitle: "-")
}
}
private enum Const {
static let titleStyle: StringStyle = .init(.font(UIFont.boldSystemFont(ofSize: 30.0)), .color(.gray))
static let subTitleStyle: StringStyle = .init(.font(UIFont.boldSystemFont(ofSize: 20.0)), .color(.lightGray))
}
let titleNode = ASTextNode.init()
let subTitleNode = ASTextNode.init()
override init() {
super.init()
automaticallyManagesSubnodes = true
}
public func update(_ state: State) {
titleNode.update({
$0.attributedText = state.title.styled(with: Const.titleStyle)
})
subTitleNode.update({
$0.attributedText = state.subTitle.styled(with: Const.subTitleStyle)
})
}
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
let stackLayout = ASStackLayoutSpec.init(
direction: .vertical,
spacing: 20.0,
justifyContent: .center,
alignItems: .center,
children: [
titleNode,
subTitleNode
]
)
return ASInsetLayoutSpec.init(
insets: .zero,
child: stackLayout
)
}
}
Controller
final class ViewController: ASViewController<ASDisplayNode> {
let testNode = Node.init()
let disposeBag = DisposeBag()
init() {
super.init(node: .init())
self.title = "Knot"
self.node.backgroundColor = .white
self.node.automaticallyManagesSubnodes = true
self.node.layoutSpecBlock = { [weak self] (_, _) -> ASLayoutSpec in
guard let self = self else { return ASLayoutSpec() }
return ASCenterLayoutSpec.init(
centeringOptions: .XY,
sizingOptions: [],
child: self.testNode
)
}
Observable<Int>
.interval(DispatchTimeInterval.milliseconds(100), scheduler: MainScheduler.instance)
.delaySubscription(DispatchTimeInterval.seconds(1), scheduler: MainScheduler.instance)
.pipe(to: testNode, {
var (integer, state) = $0
state.title = "\(integer)"
return state
})
.disposed(by: disposeBag)
Observable<Int>
.interval(DispatchTimeInterval.milliseconds(10), scheduler: MainScheduler.instance)
.delaySubscription(DispatchTimeInterval.seconds(1), scheduler: MainScheduler.instance)
.filter(with: testNode, {
return $0.0 < 100
})
.pipe(to: testNode, {
var (integer, state) = $0
state.subTitle = "\(integer)"
return state
})
.disposed(by: disposeBag)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
API Guide
Knotable
Knotable will make your node as predictable state driven node
Knotable & KnotState
By inheriting Knotable, you can design as a responsive node.
Example
class Node: ASDisplayNode & Knotable {
let titleNode = ASTextNode()
struct State: KnotState { // 1. inherit Knot State protocol
var displayTitle: String
static func defaultState() -> State {
// 2. return defaultState from static defaultState method
}
}
// 3. Use a state with updateBlock or as you know
public func update(_ state: State) {
// using updateBlock
titleNode.update({
$0.attributedText = NSAttributedString(string: state.displayTitle)
})
// or as you know
titleNode.attributedText = NSAttributedString(string: state.displayTitle)
// you don't needs call setNeedsLayout: :)
}
}
State objects can be separated to the outside.
Example
struct SomeState: KnotState {
var displayTitle: String
static func defaultState() -> State {
return .init(displayTitle: "-")
}
}
class Node: ASDisplayNode & Knotable {
typealias State = SomeState
let titleNode = ASTextNode()
public func update(_ state: State) {
titleNode.update({
$0.attributedText = NSAttributedString(string: state.displayTitle)
})
}
}
Sink
You can set state directly as sink:
let node = KnotableNode()
node.sink(State.init(...))
Stream
You can set state from observable with stream property. In this case, you don't needs call setNeedsLayout :)
Observable.just(State.init(...)).bind(to: node.stream)
ObservableType convenience extension APIs
pipe(to: KnotableNode)
If observable or subject element is KnotSate then you can just use pipe(to:) it equal to bind(to: knotableNode.stream)
let node: Knotable & SomeNode = .init(...)
Observable.just(State.init(...))
.pipe(to: node)
.disposed(by: disposeBag)
// equal
Observable.just(State.init(...))
.bind(to: node.stream)
.disposed(by: disposeBag)
Alternatively, You can reduce knotable node state with event
Observable.just(100)
.pipe(to: node, {
var (event, state) = $0
state.count = event
return state
})
.disposed(by: disposeBag)
filter(with: KnotableNode)
You can filter event with node state
Observable.just(100)
.filter(with: node, { (event, state) -> Bool in
return event == state.count
}
//...
withState(from: KnotableNode)
You can get state with event
Observable.just(100).withState(from: node) // 100, state
state(from: KnotableNode)
You can get state without event
Observable.just(100).state(from: node) // state
Example
let testNode = TestNode()
testNode.rx.tap
.state(from: testNode)
.subscribe(onNext: { state in
// TODO
})
.disposed(by: testNode.disposeBag)
Requirements
- Xcode 10.x
- Swift 5.x
- RxSwift/Cocoa 5.x
Installation
Knot is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'Knot'
Author
Geektree0101, [email protected]
License
Knot is available under the MIT license. See the LICENSE file for more info.