SkillAgentSearch skills...

Puyopuyo

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

Install / Use

/learn @scubers/Puyopuyo
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

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<br/> .fixed: fixed size<br/> .wrap: wrap contents<br/> .ratio fill up the residual space ratio, .aspectRatio: width / height<br/> 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.

View on GitHub
GitHub Stars36
CategoryDevelopment
Updated1y ago
Forks5

Languages

Swift

Security Score

80/100

Audited on Jun 9, 2024

No findings