Skip to content

A reactive and declarative layout library for UIKit written in swift.

License

Notifications You must be signed in to change notification settings

scubers/puyopuyo

Repository files navigation

Puyopuyo

CI Status Version Platform

中文

Introduce:

Youtube: https://youtu.be/3MOBCtIfRFA

Bilibili: https://www.bilibili.com/video/BV1Kh411J7vJ

Communicate:

Telegram: https://t.me/swift_puyopuyo

QQ: 830599565

Description

A declaretive layout library for UIKit base on data driven written in swift.

Requirements

swift 5.1

Installation

pod 'Puyopuyo'

Layout cost

LinearLayout

- 5 10 30 50 80 100 120 150 180 200
Puyopuyo 0.133 0.2081 0.5259 0.7739 1.141 1.3921 1.6517 2.0799 2.4759 2.7499
Yoga 0.1549 0.272 0.678 1.0211 1.4729 1.8479 2.202 2.7499 3.2939 3.664
TangramKit 0.1859 0.3151 0.7939 1.1901 1.7089 2.1331 2.5599 3.201 3.8499 4.2488
UIStackView 0.5679 0.699 1.7571 2.7871 5.0063 6.901 9.392 13.741 19.3428 23.3399

LinearLayoutImage

FlowLayout

- 3 5 10 50 80 100 120 150 180 200
Puyopuyo 0.1299 0.1292 0.2298 0.9217 1.398 1.7228 2.0749 2.5968 3.1189 3.4549
Yoga 0.1039 0.1449 0.262 1.132 1.8072 2.2501 2.7101 3.39 4.0788 4.549
TangramKit 0.1339 0.123 0.209 0.9109 1.4457 1.7881 2.1469 2.675 3.2138 3.5629

FlowLayoutImage

View depth

- 4 8 12 16 20 24 28 32 36 40
Puyopuyo 1.0719 1.5108 1.7547 1.6169 1.4448 1.739 2.0349 2.2962 2.671 2.8882
Yoga 2.3851 4.7698 6.6361 8.4819 11.2879 16.4551 23.0069 31.568 42.9677 55.4869
TangramKit 1.7189 2.711 2.932 3.117 3.7751 4.508 5.2759 6.1821 6.8421 7.6298

ViewDepthLayoutImage

Usage

A simple cell can be implemented like below. The subviews will be layout by specific rules. The buildin box follow the FlexBox rules.

/**
 VBox            HBox
 |-----------|   |-------------------|
 |Title      |   |Title   Description|
 |           |   |-------------------|
 |Description|
 |-----------|
 */
 
VBox().attach {
    UILabel().attach($0)
        .text("Title")
        .fontSize(20, weight: .bold)

    UILabel().attach($0)
        .text("Description")
}
.space(20)

// or
/**
VFlow       HFlow
|-------|   |-------|
|0  1  2|   |0  3  6|
|3  4  5|   |1  4  7|
|6  7   |   |2  5   |
|-------|   |-------|
 */
VFlow(count: 3).attach {
    for i in 0..<8 {
        UILabel().attach($0)
            .text(i.description)
    }
}
.space(20)

Library provide three buildin layout

LinearBox, FlowBox, ZBox

BoxView
    |-- ZBox
    |-- LinearBox
        |-- HBox
        |-- VBox
    |-- FlowBox
        |-- HFlow
        |-- VFlow
    

The core of the layout is the description of View nodes. Measure Regulator

Measure properties

Property Description Value
margin Current view's margin UIEdgeInset, default: .zero
alignment The alignment in superview .none, .left, .top, .bottom, .vertCenter, .horzCenter, default: .none
size Size description SizeDescription
.fixed: fixed size
.wrap: wrap contents
.ratio fill up the residual space ratio, .aspectRatio: width / height
default: .wrap, Demo:Size Properties
flowEnding Only works in FlowBox, and the arranceCount = 0, it means that current view is the last view in the row Bool, default: false
activated If will be calculate by parent box Bool, default: true

Regulator & ZBox properties

ZBox extends Regulator

Property Description Value
justifyContent Control all subview's alignment, if subview has set an alignment value, will be override by alignment .left, .top, .bottom, .vertCenter, .horzCenter, default: .none
padding Current box's padding UIEdgeInset, default: .zero

LinearRegulator properties

LinearRegulator extends Regulator

Property Description Value
space Control the space between subviews CGFloat, default: 0
format Control the main axis alignment of subviews .leading, .center, .between, .round, .trailing, default: .leading
reverse If reverse the order from adding Bool default: false

FlowRegulator properties

FlowRegulator extends LinearRegulator

Property Description Value
arrange Control the arrange count in each row, when arrange = 0, will be separate by contents Int, default: 0
itemSpace The space between items CGFloat, default: 0
runSpace The space between rows CGFloat, default: 0
format The format of the each row .leading, .center, .between, .round, .trailing, default: .leading
runFormat The format of rows .leading, .center, .between, .round, .trailing, default: .leading
runRowSize The row size in run direction (Int) -> SizeDescription, default: .wrap(shrink: 1)

DataDriven

Library provide a data driven api, to keep your UI always representing the right data. And relayout when something changes.

let text = State("")
    
VBox().attach {
    UILabel().attach($0)
        .text("Title")

    UILabel().attach($0)
        .text(text)
}

// do when some data come back
text.value = "My Description"

// if you are using RxSwift that would be another good choise
// make `Observable` implements Outputing protocol
extension Observable: Outputing {
    public typealias OutputType = Element
    /// .... some code
}

func getDescription() -> Observable<String> {
    // get some description by network or other async works
}

// use in view declare
UILabel().attach($0)
    .text(getDescription())

PS: The dataDriven api is follow the RxSwift's logic, Be aware of the memory leaks

Animations

UIView has an extension property, is an instance of protocol Animator {}

After BoxView layout, each subview's center, bounds will be assigned, The box will create animation for each view if needed.

PS: If subview has no animation, will use the nearest superview's animation, the superview must be instance of BoxView

public struct ExpandAnimator: Animator {
    public init(duration: TimeInterval = 0.3) {
        self.duration = duration
    }

    public var duration: TimeInterval
    public func animate(_ view: UIView, size: CGSize, center: CGPoint, animations: @escaping () -> Void) {
        let realSize = view.bounds.size
        let realCenter = view.center
        if realSize != size || realCenter != center {
            if realSize == .zero, realCenter == .zero {
                runAsNoneAnimation {
                    view.center = center
                    let scale: CGFloat = 0.5
                    view.bounds.size = CGSize(width: size.width * scale, height: size.height * scale)
                    view.layer.transform = CATransform3DMakeRotation(.pi / 8 + .pi, 0, 0, 1)
                }
            }

            UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 2, initialSpringVelocity: 5, options: [.curveEaseOut, .overrideInheritedOptions, .overrideInheritedDuration], animations: {
                animations()
                if realSize == .zero, realCenter == .zero {
                    view.layer.transform = CATransform3DIdentity
                }
            }, completion: nil)
        } else {
            animations()
        }
    }
}

Demo: Animation

Custom View

Because of the BoxView also a view, feel free to subclassing a Box, to reduce the view's hierarchy

class MyView: VBox {
    override func buildBody() {
        // do your view declare here
    }
}

State and Event

A complex view also need to be provide a data, and create some events, like click. In UIKit, also call DataSource and Delegate

Library provide a code pattern to defind a data and emit events.

class MyView: VBox, Stateful, Eventable {
    // declare your dataSource you need
    struct ViewState {
        var name: String?
        var title: String?
    }
    // declare event that will occured
    enum Event {
        case onConfirmed
    }
    /// declare in Stateful implement by this class
    let state = State(ViewState())
    /// declare in Eventable implement by this class
    let emitter = SimpleIO<Event>()

    override func buildBody() {
        attach {
            UILabel().attach($0)
                .text(state.map(\.name)) // use map to transform value

            UILabel().attach($0)
                .text(binder.title) // use binder dynamic member lookup to find value

            UIButton().attach($0)
                .text("Confirm")
                .bind(event: .touchUpInside, input: emitter.asInput { _ in .onConfirmed })
        }
    }
}

// Use view

let state = State(MyView.ViewState())

MyView().attach($0)
    .state(state) // bind your data source
    .onEvent(Inputs { event in
        // do your logic when event occured
    })
    

Styles

Demo: Style

// declare style
let styles: [Style] = [
    (\UIView.backgroundColor).getStyle(with: .white),
    TapRippleStyle(),
    (\UILabel.text).getStyle(with: "Click"),
    (\UIView.isUserInteractionEnabled).getStyle(with: true)
]
// Use style
UILabel().attach()
    .styles(styles)

Extension

See Puyo+xxx.swift

You can extension more depende on your needs, expect your contribution

Author

Jrwong, jr-wong@qq.com

License

Puyopuyo is available under the MIT license. See the LICENSE file for more info.

About

A reactive and declarative layout library for UIKit written in swift.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages