├── .gitignore ├── Demo └── Playground │ ├── LegoKit Documentation.docc │ ├── Documentation.md │ └── Tutorial │ │ ├── How to use LegoKit.tutorial │ │ └── Chapter-1.tutorial │ ├── Playground │ ├── Resource │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ └── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ ├── Info.plist │ ├── Playground.entitlements │ └── Source │ │ ├── Application │ │ └── AppDelegate.swift │ │ ├── ToggleViewController.swift │ │ ├── Model │ │ └── Model.swift │ │ ├── Item │ │ ├── GroupCell.swift │ │ └── UserCell.swift │ │ └── ViewController.swift │ └── Playground.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ └── octree.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ ├── xcuserdata │ └── octree.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist │ └── project.pbxproj ├── Lego.xcworkspace ├── xcuserdata │ └── octree.xcuserdatad │ │ ├── UserInterfaceState.xcuserstate │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── xcshareddata │ ├── IDEWorkspaceChecks.plist │ ├── swiftpm │ │ └── Package.resolved │ └── IDETemplateMacros.plist └── contents.xcworkspacedata ├── .swiftpm └── xcode │ ├── package.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ └── octree.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ └── octree.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── Tests └── LegoKitTests │ └── LegoKitTests.swift ├── Package.swift ├── LICENSE ├── Sources └── LegoKit │ ├── State │ ├── LegoContainer.swift │ ├── ObservableObject.swift │ ├── LegoPublished.swift │ ├── Binding.swift │ ├── StateObject.swift │ └── State.swift │ ├── Core │ ├── SectionBuilder.swift │ ├── Section.swift │ ├── Lego.swift │ ├── ItemBuilder.swift │ ├── Core.swift │ └── LegoRenderer.swift │ └── Utils │ └── CFRunLoop+Extensions.swift ├── .swiftlint.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /Demo/Playground/LegoKit Documentation.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``LegoKit`` 2 | 3 | 4 | -------------------------------------------------------------------------------- /Demo/Playground/Playground/Resource/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Lego.xcworkspace/xcuserdata/octree.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/octree/LegoKit/HEAD/Lego.xcworkspace/xcuserdata/octree.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Playground/Playground/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcuserdata/octree.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/octree/LegoKit/HEAD/.swiftpm/xcode/package.xcworkspace/xcuserdata/octree.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Demo/Playground/Playground.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Playground/Playground/Resource/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/Playground/Playground.xcodeproj/project.xcworkspace/xcuserdata/octree.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/octree/LegoKit/HEAD/Demo/Playground/Playground.xcodeproj/project.xcworkspace/xcuserdata/octree.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Lego.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Playground/Playground.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Playground/LegoKit Documentation.docc/Tutorial/How to use LegoKit.tutorial: -------------------------------------------------------------------------------- 1 | @Tutorials(name: "使用 LegoKit 配置 UICollectionView") { 2 | @Intro(title: "使用声明式语法配置 UICollectionView") { 3 | text 4 | } 5 | 6 | @Chapter(name: "创建工程") { 7 | 创建一个 iOS App 工程,添加 ``LegoKit`` 依赖 8 | @TutorialReference(tutorial: "doc:Chapter-1") 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Demo/Playground/Playground/Playground.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Tests/LegoKitTests/LegoKitTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import LegoKit 3 | 4 | final class LegoKitTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(LegoKit().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Lego.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "SnapKit", 6 | "repositoryURL": "https://github.com/SnapKit/SnapKit", 7 | "state": { 8 | "branch": null, 9 | "revision": "f222cbdf325885926566172f6f5f06af95473158", 10 | "version": "5.6.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcuserdata/octree.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | LegoKit.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Lego.xcworkspace/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // ___FILENAME___ 8 | // LegoKit 9 | // 10 | // Created by ___USERNAME___ on ___DATE___. 11 | // 12 | // Copyright (c) ___YEAR___ Octree <fouljz@gmail.com> 13 | 14 | 15 | -------------------------------------------------------------------------------- /Demo/Playground/Playground.xcodeproj/xcuserdata/octree.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Playground.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Demo/Playground/Playground/Source/Application/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Playground 4 | // 5 | // Created by Octree on 2022/3/10. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | var window: UIWindow? 13 | 14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 15 | // Override point for customization after application launch. 16 | return true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Lego.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 10 | 12 | 13 | 14 | 17 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Demo/Playground/LegoKit Documentation.docc/Tutorial/Chapter-1.tutorial: -------------------------------------------------------------------------------- 1 | @Tutorial(time: 10) { 2 | @Intro(title: "创建工程") { 3 | 创建一个 iOS App 工程,添加 ``LegoKit`` 依赖 4 | } 5 | 6 | @Section(title: "Hello") { 7 | @ContentAndMedia { 8 | text 9 | 10 | @Image(source: file, alt: "accessible description") 11 | } 12 | 13 | @Steps { 14 | @Step { 15 | text 16 | } 17 | 18 | @Step { 19 | 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "LegoKit", 8 | platforms: [ 9 | .macCatalyst(.v14), 10 | .iOS(.v13) 11 | ], 12 | products: [ 13 | .library( 14 | name: "LegoKit", 15 | targets: ["LegoKit"] 16 | ) 17 | ], 18 | dependencies: [], 19 | targets: [ 20 | .target( 21 | name: "LegoKit", 22 | dependencies: [] 23 | ), 24 | .testTarget( 25 | name: "LegoKitTests", 26 | dependencies: ["LegoKit"] 27 | ) 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /Lego.xcworkspace/xcuserdata/octree.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Octree 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Demo/Playground/Playground/Source/ToggleViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToggleViewController.swift 3 | // LegoKit 4 | // 5 | // Created by octree on 2022/5/5. 6 | // 7 | // Copyright (c) 2022 Octree 8 | 9 | import LegoKit 10 | import UIKit 11 | 12 | public final class ToggleViewController: UIViewController { 13 | @Binding var flag: Bool 14 | 15 | public init(flag: Binding) { 16 | _flag = flag 17 | super.init(nibName: nil, bundle: nil) 18 | } 19 | 20 | @available(*, unavailable) 21 | required init?(coder: NSCoder) { 22 | fatalError("init(coder:) has not been implemented") 23 | } 24 | 25 | override public func viewDidLoad() { 26 | super.viewDidLoad() 27 | if #available(iOS 13.0, *) { 28 | view.backgroundColor = .systemBackground 29 | } else { 30 | view.backgroundColor = .white 31 | } 32 | let toggleView = UISwitch(frame: CGRect(origin: .init(x: 100, y: 100), 33 | size: .init(width: 100, height: 40))) 34 | toggleView.addTarget(self, action: #selector(toggle), for: .primaryActionTriggered) 35 | view.addSubview(toggleView) 36 | toggleView.isOn = flag 37 | } 38 | 39 | @objc func toggle(sender: UISwitch) { 40 | flag = sender.isOn 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Lego.xcworkspace/xcuserdata/octree.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SnapKitPlayground (Playground) 1.xcscheme 8 | 9 | isShown 10 | 11 | orderHint 12 | 3 13 | 14 | SnapKitPlayground (Playground) 2.xcscheme 15 | 16 | isShown 17 | 18 | orderHint 19 | 4 20 | 21 | SnapKitPlayground (Playground) 3.xcscheme 22 | 23 | isShown 24 | 25 | orderHint 26 | 5 27 | 28 | SnapKitPlayground (Playground) 4.xcscheme 29 | 30 | isShown 31 | 32 | orderHint 33 | 6 34 | 35 | SnapKitPlayground (Playground) 5.xcscheme 36 | 37 | isShown 38 | 39 | orderHint 40 | 7 41 | 42 | SnapKitPlayground (Playground).xcscheme 43 | 44 | isShown 45 | 46 | orderHint 47 | 2 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Demo/Playground/Playground/Source/Model/Model.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Model.swift 3 | // LegoKit 4 | // 5 | // Created by octree on 2022/3/11. 6 | // 7 | // Copyright (c) 2022 Octree 8 | 9 | import Foundation 10 | 11 | public enum Gender: CaseIterable { 12 | case male 13 | case female 14 | } 15 | 16 | extension Gender: CustomStringConvertible { 17 | public var description: String { 18 | switch self { 19 | case .male: 20 | return "男" 21 | case .female: 22 | return "女" 23 | } 24 | } 25 | } 26 | 27 | extension Gender { 28 | static var random: Gender { allCases.randomElement()! } 29 | } 30 | 31 | public struct Group: Equatable { 32 | public var id: UUID = .init() 33 | public var name: String 34 | public var users: [User] 35 | } 36 | 37 | public struct User: Equatable { 38 | public var id: UUID = .init() 39 | public var name: String 40 | public var gender: Gender = .random 41 | } 42 | 43 | extension Group { 44 | static var initialGroups: [Group] { 45 | [ 46 | .init(name: "iOS", users: [ 47 | User(name: "Octree"), 48 | User(name: "Chris"), 49 | User(name: "James") 50 | ]), 51 | .init(name: "Android", users: [ 52 | User(name: "A"), 53 | User(name: "B"), 54 | User(name: "C") 55 | ]) 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Demo/Playground/Playground/Source/Item/GroupCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupCell.swift 3 | // LegoKit 4 | // 5 | // Created by octree on 2022/3/11. 6 | // 7 | // Copyright (c) 2022 Octree 8 | 9 | import LegoKit 10 | import UIKit 11 | import SnapKit 12 | 13 | // MARK: - Item 14 | 15 | /// Item 用来描述渲染一种 Cell 需要携带的数据。 16 | /// 并从类型和 ``GroupCell`` 进行绑定 17 | public struct GroupItem: TypedItemType { 18 | // 绑定 Cell 的类型 19 | public typealias CellType = GroupCell 20 | // 预留 ID,为以后做准备 21 | public var id: UUID 22 | // 分组名称 23 | public var name: String 24 | } 25 | 26 | // MARK: - Cell 27 | 28 | public class GroupCell: UICollectionViewCell, TypedCellType { 29 | public typealias Item = GroupItem 30 | 31 | // MARK: - Properties 32 | 33 | private lazy var nameLabel: UILabel = .init() 34 | 35 | // MARK: - Life Cyle 36 | 37 | // MARK: Initializer 38 | 39 | override public init(frame: CGRect) { 40 | super.init(frame: frame) 41 | setup() 42 | } 43 | 44 | public required init?(coder: NSCoder) { 45 | super.init(coder: coder) 46 | setup() 47 | } 48 | 49 | // MARK: - TypedCellType 50 | 51 | public func update(with item: Item) { 52 | nameLabel.text = item.name 53 | } 54 | 55 | // MARK: - Private 56 | 57 | // MARK: Setup 58 | 59 | private func setup() { 60 | contentView.addSubview(nameLabel) 61 | nameLabel.snp.makeConstraints { 62 | $0.leading.equalToSuperview().offset(24) 63 | $0.top.equalToSuperview().offset(8) 64 | $0.bottom.equalToSuperview().offset(-8).priority(.low) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/LegoKit/State/LegoContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LegoContainer.swift 3 | // LegoKit 4 | // 5 | // Created by octree on 2022/3/11. 6 | // 7 | // Copyright (c) 2022 Octree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | 29 | /// A protocol defines the part of a ``LegoContainer`` type. 30 | /// You can make your ``UIViewController`` confirm to this protocol, 31 | /// so that You can use ``@State``、``@StateObject``. 32 | @MainActor 33 | public protocol LegoContainer: AnyObject { 34 | var lego: Lego { get } 35 | var legoRenderer: LegoRenderer { get } 36 | } 37 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | opt_in_rules: 2 | - vertical_parameter_alignment_on_call 3 | 4 | disabled_rules: 5 | - force_try 6 | - force_cast 7 | - nesting 8 | - block_based_kvo 9 | - opening_brace 10 | - no_fallthrough_only 11 | - for_where 12 | - unhandled_throwing_task 13 | - unneeded_synthesized_initializer 14 | 15 | analyzer_rules: 16 | - unused_declaration 17 | - unused_import 18 | 19 | excluded: 20 | - .build 21 | - "**/Package.swift" 22 | - "**/Generated" 23 | 24 | 25 | identifier_name: 26 | max_length: 27 | warning: 60 28 | error: 100 29 | min_length: 30 | warning: 2 31 | excluded: 32 | - x 33 | - y 34 | - z 35 | 36 | type_name: 37 | max_length: 38 | warning: 60 39 | error: 100 40 | min_length: 41 | warning: 2 42 | 43 | function_parameter_count: 44 | warning: 6 45 | 46 | vertical_whitespace: 47 | max_empty_lines: 2 48 | 49 | file_length: 50 | warning: 2000 51 | error: 3000 52 | 53 | line_length: 54 | warning: 1000 55 | error: 2000 56 | 57 | type_body_length: 58 | warning: 1000 59 | error: 1500 60 | 61 | function_body_length: 62 | warning: 300 63 | error: 500 64 | 65 | cyclomatic_complexity: 66 | warning: 40 67 | error: 50 68 | 69 | large_tuple: 70 | warning: 3 71 | error: 6 72 | 73 | custom_rules: 74 | comment_space: 75 | name: "Space After Comment" 76 | regex: '//\S' 77 | match_kinds: 78 | - comment 79 | message: "There should be a space after a comment delimiter." 80 | severity: warning 81 | 82 | inline_comment_spaces: 83 | name: "Spaces Before Inline Comment" 84 | regex: '\S ?//' 85 | match_kinds: 86 | - comment 87 | message: "There should be more than 2 spaces before an inline comment." 88 | severity: warning 89 | reporter: "xcode" -------------------------------------------------------------------------------- /Demo/Playground/Playground/Resource/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Sources/LegoKit/State/ObservableObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableObject.swift 3 | // LegoKit 4 | // 5 | // Created by octree on 2022/3/11. 6 | // 7 | // Copyright (c) 2022 Octree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Combine 28 | import UIKit 29 | 30 | public protocol LegoObservableObject: AnyObject { 31 | var objectDidChange: PassthroughSubject { get } 32 | } 33 | 34 | private enum AssociatedKeys { 35 | static let publisher = malloc(1)! 36 | } 37 | 38 | public extension LegoObservableObject { 39 | var objectDidChange: PassthroughSubject { 40 | var publisher = objc_getAssociatedObject(self, AssociatedKeys.publisher) as? PassthroughSubject 41 | if publisher == nil { 42 | publisher = .init() 43 | objc_setAssociatedObject(self, AssociatedKeys.publisher, publisher, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 44 | } 45 | return publisher! 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Demo/Playground/Playground/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Demo/Playground/Playground/Source/Item/UserCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserCell.swift 3 | // LegoKit 4 | // 5 | // Created by octree on 2022/3/11. 6 | // 7 | // Copyright (c) 2022 Octree 8 | 9 | import LegoKit 10 | import UIKit 11 | import SnapKit 12 | 13 | // MARK: - Item 14 | 15 | /// Item 用来描述渲染一种 Cell 需要携带的数据。 16 | /// 并从类型和 ``UserCell`` 进行绑定 17 | public struct UserItem: TypedItemType { 18 | // 绑定 Cell 的类型 19 | public typealias CellType = UserCell 20 | // 预留 ID,为以后做准备 21 | public var id: UUID 22 | // 用户名称 23 | public var name: String 24 | public var gender: Gender 25 | } 26 | 27 | // MARK: - Cell 28 | 29 | public class UserCell: UICollectionViewCell, TypedCellType { 30 | public typealias Item = UserItem 31 | 32 | // MARK: - Properties 33 | 34 | private lazy var nameLabel: UILabel = .init() 35 | private lazy var genderLabel: UILabel = { 36 | let label = UILabel() 37 | label.font = .systemFont(ofSize: 12) 38 | label.textColor = .lightGray 39 | return label 40 | }() 41 | private let containerView = UIView() 42 | 43 | // MARK: - Life Cyle 44 | 45 | // MARK: Initializer 46 | 47 | override public init(frame: CGRect) { 48 | super.init(frame: frame) 49 | setup() 50 | } 51 | 52 | public required init?(coder: NSCoder) { 53 | super.init(coder: coder) 54 | setup() 55 | } 56 | 57 | // MARK: - TypedCellType 58 | 59 | public func update(with item: Item) { 60 | nameLabel.text = item.name 61 | genderLabel.text = "性别:\(item.gender)" 62 | } 63 | 64 | // MARK: - Private 65 | 66 | // MARK: Setup 67 | 68 | private func setup() { 69 | nameLabel.numberOfLines = 0 70 | contentView.addSubview(nameLabel) 71 | nameLabel.snp.makeConstraints { 72 | $0.leading.equalToSuperview().offset(24) 73 | $0.top.equalToSuperview().offset(16) 74 | $0.trailing.equalToSuperview().offset(-24) 75 | } 76 | 77 | contentView.addSubview(genderLabel) 78 | genderLabel.snp.makeConstraints { 79 | $0.leading.equalToSuperview().offset(24) 80 | $0.top.equalTo(nameLabel.snp.bottom).offset(8) 81 | $0.trailing.equalToSuperview().offset(-24) 82 | $0.bottom.equalToSuperview().offset(-16).priority(.low) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/LegoKit/State/LegoPublished.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LegoPublished.swift 3 | // LegoKit 4 | // 5 | // Created by octree on 2022/3/11. 6 | // 7 | // Copyright (c) 2022 Octree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | 29 | /// The property wrapped is always used within a ``LegoObservableObject`` 30 | /// It will call the ``LegoObservableObject/objectDidChange`` after the wrapped value is changed. 31 | @propertyWrapper 32 | public struct LegoPublished { 33 | public static subscript( 34 | _enclosingInstance instance: T, 35 | wrapped wrappedKeyPath: ReferenceWritableKeyPath, 36 | storage storageKeyPath: ReferenceWritableKeyPath 37 | ) -> Value { 38 | get { 39 | instance[keyPath: storageKeyPath].storage 40 | } 41 | set { 42 | instance[keyPath: storageKeyPath].storage = newValue 43 | instance.objectDidChange.send() 44 | } 45 | } 46 | 47 | @available(*, unavailable, 48 | message: "@LegoPublished can only be applied to classes") 49 | public var wrappedValue: Value { 50 | get { fatalError() } 51 | // swiftlint:disable unused_setter_value 52 | set { fatalError() } 53 | // swiftlint:enable unused_setter_value 54 | } 55 | 56 | private var storage: Value 57 | 58 | public init(wrappedValue: Value) { 59 | storage = wrappedValue 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/LegoKit/Core/SectionBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionBuilder.swift 3 | // LegoKit 4 | // 5 | // Created by octree on 2022/3/11. 6 | // 7 | // Copyright (c) 2022 Octree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | 29 | @resultBuilder 30 | public enum SectionBuilder { 31 | public static func buildExpression(_ expression: Void) -> [AnySection] { 32 | [] 33 | } 34 | 35 | public static func buildExpression(_ expression: Section) -> [AnySection] { 36 | [AnySection(expression)] 37 | } 38 | 39 | public static func buildBlock(_ components: [AnySection]...) -> [AnySection] { 40 | components.flatMap { $0 } 41 | } 42 | 43 | public static func buildIf(_ components: AnySection?...) -> [AnySection] { 44 | components.compactMap { $0 } 45 | } 46 | 47 | public static func buildEither(first component: [AnySection]) -> [AnySection] { 48 | component 49 | } 50 | 51 | public static func buildEither(second component: [AnySection]) -> [AnySection] { 52 | component 53 | } 54 | 55 | public static func buildArray(_ components: [[AnySection]]) -> [AnySection] { 56 | components.flatMap { $0 } 57 | } 58 | 59 | public static func buildLimitedAvailability(_ component: [AnySection]) -> [AnySection] { 60 | component 61 | } 62 | 63 | public static func buildOptional(_ component: [AnySection]?) -> [AnySection] { 64 | component ?? [] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/LegoKit/Core/Section.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Section.swift 3 | // LegoKit 4 | // 5 | // Created by octree on 2022/3/11. 6 | // 7 | // Copyright (c) 2022 Octree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | 29 | public struct Section { 30 | public var id: ID 31 | var layout: (NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection 32 | public var items: [AnyItem] = [] 33 | 34 | public init(id: ID, layout: NSCollectionLayoutSection, @ItemBuilder items: () -> [AnyItem]) { 35 | self.id = id 36 | self.layout = { _ in layout } 37 | self.items = items() 38 | } 39 | 40 | public init(id: ID, layout: @escaping () -> NSCollectionLayoutSection, @ItemBuilder items: () -> [AnyItem]) { 41 | self.id = id 42 | self.layout = { _ in layout() } 43 | self.items = items() 44 | } 45 | 46 | public init(id: ID, layout: @escaping (NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection, @ItemBuilder items: () -> [AnyItem]) { 47 | self.id = id 48 | self.layout = layout 49 | self.items = items() 50 | } 51 | } 52 | 53 | public struct AnySection { 54 | public var id: AnyID 55 | var layout: (NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection 56 | public var items: [AnyItem] = [] 57 | 58 | init(_ section: Section) { 59 | id = AnyID(section.id) 60 | layout = section.layout 61 | items = section.items 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/LegoKit/Core/Lego.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Lego.swift 3 | // LegoKit 4 | // 5 | // Created by octree on 2022/3/11. 6 | // 7 | // Copyright (c) 2022 Octree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | 29 | @MainActor 30 | public struct Lego { 31 | public var sections: [AnySection] 32 | 33 | public init(@SectionBuilder sections: () -> [AnySection]) { 34 | self.sections = sections() 35 | } 36 | } 37 | 38 | public extension Lego { 39 | subscript(indexPath: IndexPath) -> AnyItem { 40 | sections[indexPath.section].items[indexPath.item] 41 | } 42 | 43 | subscript(indexPath: IndexPath, as type: C.Type) -> C? { 44 | self[indexPath].as(type) 45 | } 46 | } 47 | 48 | extension Lego { 49 | func indexPathForItem(with identifier: ID) -> IndexPath? { 50 | for (sectionIndex, section) in sections.enumerated() { 51 | for (itemIndex, item) in section.items.enumerated() where item.anyID == AnyID(identifier) { 52 | return IndexPath(item: itemIndex, section: sectionIndex) 53 | } 54 | } 55 | return nil 56 | } 57 | 58 | func indexPathForItem(with identifier: AnyID) -> IndexPath? { 59 | for (sectionIndex, section) in sections.enumerated() { 60 | for (itemIndex, item) in section.items.enumerated() where item.anyID == identifier { 61 | return IndexPath(item: itemIndex, section: sectionIndex) 62 | } 63 | } 64 | return nil 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/LegoKit/State/Binding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Binding.swift 3 | // LegoKit 4 | // 5 | // Created by octree on 2022/5/5. 6 | // 7 | // Copyright (c) 2022 Octree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | 29 | /// 这个 PropertyWrapper 可以在任何情况下使用 30 | /// 当是一个 ``LegoContainer`` 的属性时,会自动更新 ``LegoRenderer`` 31 | @propertyWrapper 32 | @MainActor 33 | public struct Binding { 34 | public var wrappedValue: Value { 35 | get { get() } 36 | set { set(newValue) } 37 | } 38 | 39 | private var get: () -> Value 40 | private var set: (Value) -> Void 41 | 42 | public var projectedValue: Binding { 43 | self 44 | } 45 | 46 | /// Create a property wrapper with initial value. 47 | /// - Parameter wrappedValue: The initial value. 48 | public init(get: @escaping () -> Value, set: @escaping (Value) -> Void) { 49 | self.get = get 50 | self.set = set 51 | } 52 | 53 | public static subscript( 54 | _enclosingInstance instance: T, 55 | wrapped wrappedKeyPath: ReferenceWritableKeyPath, 56 | storage storageKeyPath: ReferenceWritableKeyPath 57 | ) -> Value { 58 | get { 59 | return instance[keyPath: storageKeyPath].get() 60 | } 61 | set { 62 | instance[keyPath: storageKeyPath].set(newValue) 63 | if let container = instance as? LegoContainer { 64 | container.legoRenderer.apply(lego: container.lego) 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/LegoKit/Core/ItemBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemBuilder.swift 3 | // LegoKit 4 | // 5 | // Created by octree on 2022/3/11. 6 | // 7 | // Copyright (c) 2022 Octree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | 29 | @resultBuilder 30 | @MainActor 31 | public enum ItemBuilder { 32 | public static func buildExpression(_ expression: Void) -> [AnyItem] { 33 | [] 34 | } 35 | 36 | public static func buildExpression(_ expression: AnyItem) -> [AnyItem] { 37 | [expression] 38 | } 39 | 40 | public static func buildExpression(_ expression: C) -> [AnyItem] { 41 | [AnyItem(expression)] 42 | } 43 | 44 | public static func buildExpression(_ expression: [AnyItem]) -> [AnyItem] { 45 | expression 46 | } 47 | 48 | public static func buildExpression(_ expression: [C]) -> [AnyItem] { 49 | expression.map { AnyItem($0) } 50 | } 51 | 52 | public static func buildBlock(_ components: [AnyItem]...) -> [AnyItem] { 53 | Array(components.joined()) 54 | } 55 | 56 | public static func buildIf(_ components: AnyItem?...) -> [AnyItem] { 57 | components.compactMap { $0 } 58 | } 59 | 60 | public static func buildEither(first component: [AnyItem]) -> [AnyItem] { 61 | component 62 | } 63 | 64 | public static func buildEither(second component: [AnyItem]) -> [AnyItem] { 65 | component 66 | } 67 | 68 | public static func buildArray(_ components: [[AnyItem]]) -> [AnyItem] { 69 | components.flatMap { $0 } 70 | } 71 | 72 | public static func buildLimitedAvailability(_ component: [AnyItem]) -> [AnyItem] { 73 | component 74 | } 75 | 76 | public static func buildOptional(_ component: [AnyItem]?) -> [AnyItem] { 77 | component ?? [] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Demo/Playground/Playground/Source/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Playground 4 | // 5 | // Created by Octree on 2022/3/10. 6 | // 7 | 8 | import LegoKit 9 | import UIKit 10 | 11 | class ViewModel: LegoObservableObject { 12 | @LegoPublished var groups: [Group] = Group.initialGroups 13 | 14 | func addMember() { 15 | let alpha = "abcdefghijklmnopqrstuvwxyz" 16 | let name = String((0 ... 8).map { _ in alpha.randomElement()! }).capitalized 17 | groups[0].users.append(.init(name: name)) 18 | } 19 | } 20 | 21 | class ViewController: UIViewController, LegoContainer { 22 | @StateObject var viewModel = ViewModel() 23 | @State var flag: Bool = true 24 | 25 | private var sectionLayout: NSCollectionLayoutSection = { 26 | let size: NSCollectionLayoutSize = .init(widthDimension: .fractionalWidth(1), heightDimension: .estimated(40)) 27 | let item = NSCollectionLayoutItem(layoutSize: size) 28 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitem: item, count: 1) 29 | let section = NSCollectionLayoutSection(group: group) 30 | section.interGroupSpacing = 1 31 | return section 32 | }() 33 | 34 | var lego: Lego { 35 | Lego { 36 | for group in viewModel.groups { 37 | Section(id: group.id, layout: sectionLayout) { 38 | if flag { 39 | GroupItem(id: group.id, name: group.name) 40 | } 41 | for user in group.users { 42 | UserItem(id: user.id, name: user.name, gender: user.gender) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | lazy var legoRenderer: LegoRenderer = .init(lego: lego) 50 | 51 | override func viewDidLoad() { 52 | super.viewDidLoad() 53 | legoRenderer.render(in: view) { 54 | $0.backgroundColor = UIColor(white: 0.95, alpha: 1) 55 | $0.alwaysBounceVertical = true 56 | } 57 | 58 | let barItem = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(toggle)) 59 | let push = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(push)) 60 | navigationItem.rightBarButtonItems = [push, barItem] 61 | 62 | let updateItem = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(updateItem)) 63 | let addItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addMember)) 64 | navigationItem.leftBarButtonItems = [addItem, updateItem] 65 | } 66 | 67 | @objc private func updateItem() { 68 | viewModel.groups[0].users[0].name += " Octree" 69 | } 70 | 71 | @objc private func toggle() { 72 | flag.toggle() 73 | } 74 | 75 | @objc private func addMember() { 76 | viewModel.addMember() 77 | } 78 | 79 | @objc private func push() { 80 | let vc = ToggleViewController(flag: $flag) 81 | navigationController?.pushViewController(vc, animated: true) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LegoKit 2 | 3 | 使用声明式语法组织 UICollectionView。支持类似 SwiftUI 的 `@State`、`@Published` 和 `@StateObject` 4 | 5 | 6 | ## Installation 7 | 8 | ### Swift Package Manager 9 | 10 | File > Swift Packages > Add Package Dependency 11 | Add https://github.com/octree/LegoKit.git 12 | Select "Up to Next Major" with "1.0.0" 13 | 14 | 15 | ## How to Use 16 | 17 | ### Cell 的实现 18 | 19 | ```swift 20 | public struct ColorItem: TypedItemType { 21 | public typealias CellType = ColorCell 22 | public var id: UUID 23 | public var color: UIColor 24 | public var height: CGFloat 25 | } 26 | 27 | public class ColorCell: UICollectionViewCell, TypedCellType { 28 | public typealias Item = ColorItem 29 | 30 | public func update(with item: ColorItem) { 31 | backgroundColor = item.color 32 | } 33 | } 34 | ``` 35 | 36 | * id: 可以是任意 `Hashable`类型,为了给以后做动画,进行 diff 准备的预留属性。 37 | * 其他的属性,是用来配置 Cell 的信息 38 | * Cell 需要实现 `TypedCellType` 的方法 39 | * **update**:根据对应的 `Item` 更新 UI 40 | 41 | ### 渲染 UICollectionView 42 | 43 | #### 配置 Lego 44 | 45 | ```swift 46 | class ViewController: UIViewController { 47 | var lego: Lego { 48 | Lego { 49 | Section(id: ..., WaterfallLayout()) { 50 | ColorItem(...) 51 | ColorItem(...) 52 | ColorItem(...) 53 | } 54 | 55 | Section(WaterfallLayout()) { 56 | for elt in array { 57 | ColorItem(elt) 58 | } 59 | } 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | * Lego 用于描述 **CollectionView** 中的 Section 和 Cell 的信息 66 | * 使用类似 SwiftUI 的声明式语法构建 67 | * 支持 `if`、`#if @available `、`for in`、`switch case`等结构 68 | 69 | #### 渲染 70 | 71 | ```swift 72 | class ViewController: UIViewController { 73 | lazy var legoRenderer: LegoRenderer = .init(lego: lego) 74 | override func viewDidLoad() { 75 | legoRenderer.render(in: view) { 76 | $0.backgroundColor = UIColor(white: 0.95, alpha: 1) 77 | } 78 | } 79 | 80 | private func reload() { 81 | legoRenderer.apply(lego) 82 | } 83 | } 84 | ``` 85 | 86 | 87 | 88 | ### 使用 @State、@StateObject 等,自动更新 CollectionView 89 | 90 | ```swift 91 | class ViewModel: LegoObservableObject { 92 | @LegoPublished var items: [ColorItem] = [] 93 | func addRandomColor() { 94 | items.append(.random) 95 | } 96 | } 97 | 98 | class ViewController: UIViewController, LegoContainer { 99 | @StateObject var viewModel = ViewModel() 100 | @State var flag: Bool = false 101 | lazy var legoRenderer: LegoRenderer = .init(lego: lego) 102 | var lego: Lego { 103 | Lego { 104 | Section(id: 0, layout: WaterfallLayout()) { 105 | if flag { 106 | // ... some items. 107 | } 108 | viewModel.items 109 | } 110 | } 111 | } 112 | 113 | override func viewDidLoad() { 114 | super.viewDidLoad() 115 | legoRenderer.render(in: view) { 116 | $0.backgroundColor = UIColor(white: 0.95, alpha: 1) 117 | } 118 | } 119 | } 120 | ``` 121 | 122 | * 当用于配置 Lego 的数据,使用了 `@State` 或者 `@StateObject ` 时就不需要手动调用 `apply` 函数。当数据发生变动时,会自动更新 `UICollectionView`。 123 | 124 | ## License 125 | 126 | **LegoKit** is available under the MIT license. See the LICENSE file for more info. 127 | -------------------------------------------------------------------------------- /Demo/Playground/Playground/Resource/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Sources/LegoKit/State/StateObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StateObject.swift 3 | // LegoKit 4 | // 5 | // Created by octree on 2022/3/11. 6 | // 7 | // Copyright (c) 2022 Octree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Combine 28 | import Foundation 29 | 30 | /// This property wrapper will apply ``Lego`` to ``LegoRenderer`` automatically while the ``Published`` value wa changed. 31 | /// The instance that hold this property must confirm to ``LegoContainer`` protocol. 32 | /// And the value must confirm to ``LegoObservableObject`` protocol 33 | @propertyWrapper 34 | @MainActor 35 | public struct StateObject { 36 | @available(*, unavailable, 37 | message: "This property wrapper can only be applied to ``LegoContainer``") 38 | public var wrappedValue: Value { 39 | get { fatalError() } 40 | // swiftlint:disable unused_setter_value 41 | set { fatalError() } 42 | // swiftlint:enable unused_setter_value 43 | } 44 | 45 | private class Box { 46 | weak var container: LegoContainer? 47 | var cancellable: AnyCancellable? 48 | } 49 | 50 | private var storage: Value 51 | private var box: Box = .init() 52 | 53 | /// Create a property wrapper with initial value 54 | /// - Parameter wrappedValue: The object to observed. 55 | public init(wrappedValue: Value) { 56 | storage = wrappedValue 57 | } 58 | 59 | private func bind(to instance: LegoContainer) { 60 | guard box.container !== instance else { return } 61 | box.container = instance 62 | box.cancellable = storage.objectDidChange.sink { [weak box] in 63 | guard let container = box?.container else { 64 | return 65 | } 66 | container.legoRenderer.apply(lego: container.lego) 67 | } 68 | } 69 | 70 | public static subscript( 71 | _enclosingInstance instance: T, 72 | wrapped wrappedKeyPath: ReferenceWritableKeyPath, 73 | storage storageKeyPath: ReferenceWritableKeyPath 74 | ) -> Value { 75 | get { 76 | instance[keyPath: storageKeyPath].bind(to: instance) 77 | return instance[keyPath: storageKeyPath].storage 78 | } 79 | set { 80 | instance[keyPath: storageKeyPath].storage = newValue 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/LegoKit/Utils/CFRunLoop+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Binding.swift 3 | // LegoKit 4 | // 5 | // Created by octree on 2022/6/6. 6 | // 7 | // Copyright (c) 2022 Octree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Combine 28 | import CoreFoundation 29 | import Foundation 30 | 31 | private final class _AnyObserverBox { 32 | private var _trigger: () -> Void 33 | init(_ box: _ObserverBox) { 34 | _trigger = { box.trigger() } 35 | } 36 | 37 | func trigger() { 38 | _trigger() 39 | } 40 | } 41 | 42 | private final class _ObserverBox { 43 | weak var target: T? 44 | var action: (T) -> Void 45 | 46 | init(target: T, action: @escaping (T) -> Void) { 47 | self.target = target 48 | self.action = action 49 | } 50 | 51 | func trigger() { 52 | guard let target = target else { 53 | return 54 | } 55 | action(target) 56 | } 57 | 58 | func typeErased() -> _AnyObserverBox { .init(self) } 59 | } 60 | 61 | public extension CFRunLoop { 62 | func addObserver(_ target: T, 63 | activity: CFRunLoopActivity, 64 | mode: CFRunLoopMode = .defaultMode, 65 | repeat: Bool = true, 66 | order: CFIndex = 0, 67 | action: @escaping (T) -> Void) -> AnyCancellable 68 | { 69 | let box = _ObserverBox(target: target, action: action).typeErased() 70 | let info = Unmanaged.passRetained(box).toOpaque() 71 | var context = CFRunLoopObserverContext(version: 0, info: info, retain: nil, release: nil, copyDescription: nil) 72 | let observer = CFRunLoopObserverCreate(kCFAllocatorDefault, activity.rawValue, `repeat`, order, runLoopObserverCallback(), &context) 73 | CFRunLoopAddObserver(self, 74 | observer, 75 | mode) 76 | return .init { 77 | CFRunLoopRemoveObserver(self, observer, mode) 78 | } 79 | } 80 | 81 | func runLoopObserverCallback() -> CFRunLoopObserverCallBack { 82 | return { _, _, info in 83 | guard let info = info else { 84 | return 85 | } 86 | let box = Unmanaged<_AnyObserverBox>.fromOpaque(info).takeUnretainedValue() 87 | box.trigger() 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/LegoKit/State/State.swift: -------------------------------------------------------------------------------- 1 | // 2 | // State.swift 3 | // LegoKit 4 | // 5 | // Created by octree on 2022/3/11. 6 | // 7 | // Copyright (c) 2022 Octree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | 29 | final class AnyLocation { 30 | private var get: (() -> Value)! 31 | private var set: ((Value) -> Void)! 32 | 33 | var value: Value { 34 | get { get() } 35 | set { set(newValue) } 36 | } 37 | 38 | init() {} 39 | 40 | func bind(to instance: T, keyPath: ReferenceWritableKeyPath) { 41 | get = { [unowned instance] in 42 | instance[keyPath: keyPath] 43 | } 44 | set = { [unowned instance] in 45 | instance[keyPath: keyPath] = $0 46 | } 47 | } 48 | } 49 | 50 | /// This property wrapper will apply ``Lego`` to ``LegoRenderer`` automatically while the wrappedValue was changed. 51 | /// The instance that hold this property must confirm to ``LegoContainer`` protocol. 52 | @propertyWrapper 53 | @MainActor 54 | public struct State { 55 | @available(*, unavailable, 56 | message: "This property wrapper can only be applied to ``LegoContainer``") 57 | public var wrappedValue: Value { 58 | get { fatalError() } 59 | // swiftlint:disable unused_setter_value 60 | set { fatalError() } 61 | // swiftlint:enable unused_setter_value 62 | } 63 | 64 | private var location: AnyLocation = .init() 65 | private var storage: Value 66 | public var projectedValue: Binding { 67 | .init { 68 | location.value 69 | } set: { 70 | location.value = $0 71 | } 72 | } 73 | 74 | /// Create a property wrapper with initial value. 75 | /// - Parameter wrappedValue: The initial value. 76 | public init(wrappedValue: Value) { 77 | storage = wrappedValue 78 | } 79 | 80 | public static subscript( 81 | _enclosingInstance instance: T, 82 | wrapped wrappedKeyPath: ReferenceWritableKeyPath, 83 | storage storageKeyPath: ReferenceWritableKeyPath 84 | ) -> Value { 85 | get { 86 | let state = instance[keyPath: storageKeyPath] 87 | instance[keyPath: storageKeyPath].location.bind(to: instance, keyPath: wrappedKeyPath) 88 | return state.storage 89 | } 90 | set { 91 | instance[keyPath: storageKeyPath].storage = newValue 92 | instance.legoRenderer.apply(lego: instance.lego) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/LegoKit/Core/Core.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Core.swift 3 | // LegoKit 4 | // 5 | // Created by octree on 2022/3/11. 6 | // 7 | // Copyright (c) 2022 Octree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | 29 | public struct AnyID: Hashable, Sendable, Equatable { 30 | private nonisolated(unsafe) var value: Any 31 | private nonisolated(unsafe) var hashInto: (inout Hasher) -> Void 32 | private nonisolated(unsafe) var equalTo: (AnyID) -> Bool 33 | init(_ value: Value) { 34 | self.value = value 35 | hashInto = { 36 | $0.combine(value) 37 | } 38 | equalTo = { 39 | guard let other = $0.value as? Value else { return false } 40 | return other == value 41 | } 42 | } 43 | 44 | public func hash(into hasher: inout Hasher) { 45 | hashInto(&hasher) 46 | } 47 | 48 | public static func == (lhs: AnyID, rhs: AnyID) -> Bool { 49 | lhs.equalTo(rhs) 50 | } 51 | } 52 | 53 | /// A type represents the part of a lego cell 54 | @MainActor 55 | public protocol CellType {} 56 | 57 | /// A Lego cell with a specified Item type 58 | @MainActor 59 | public protocol TypedCellType: CellType { 60 | associatedtype Item 61 | func update(with item: Item) 62 | } 63 | 64 | @MainActor 65 | public protocol ItemType { 66 | var anyID: AnyID { get } 67 | func createCell(in collectionView: UICollectionView, at indexPath: IndexPath) -> UICollectionViewCell 68 | } 69 | 70 | @MainActor 71 | public struct AnyItem: ItemType { 72 | public var base: ItemType 73 | public var anyID: AnyID { base.anyID } 74 | private var _updateCell: (UICollectionViewCell) -> Void 75 | public func createCell(in collectionView: UICollectionView, at indexPath: IndexPath) -> UICollectionViewCell { 76 | base.createCell(in: collectionView, at: indexPath) 77 | } 78 | 79 | init(_ item: C) { 80 | base = item 81 | _updateCell = { item.updateCell($0) } 82 | } 83 | 84 | func updateCell(_ cell: UICollectionViewCell) { 85 | _updateCell(cell) 86 | } 87 | 88 | func `as`(_ type: C.Type) -> C? { 89 | base as? C 90 | } 91 | } 92 | 93 | @MainActor 94 | public protocol TypedItemType: ItemType { 95 | associatedtype ID: Hashable & Sendable 96 | associatedtype CellType: UICollectionViewCell, TypedCellType where CellType.Item == Self 97 | var id: ID { get } 98 | } 99 | 100 | public extension TypedItemType { 101 | var asItems: [AnyItem] { [AnyItem(self)] } 102 | } 103 | 104 | public extension TypedItemType { 105 | var anyID: AnyID { .init(id) } 106 | } 107 | 108 | public extension ItemType { 109 | func createCell(in collectionView: UICollectionView, at indexPath: IndexPath) -> UICollectionViewCell { 110 | fatalError() 111 | } 112 | } 113 | 114 | extension TypedItemType { 115 | static var identifier: String { 116 | return String(describing: CellType.self) 117 | } 118 | 119 | public func createCell(in collectionView: UICollectionView, at indexPath: IndexPath) -> UICollectionViewCell { 120 | Self.register(in: collectionView) 121 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Self.identifier, for: indexPath) as? CellType else { 122 | fatalError("Cannot create a new cell with identifier: \(Self.identifier), Expected cell type: \(CellType.self)") 123 | } 124 | cell.update(with: self) 125 | return cell 126 | } 127 | 128 | static func register(in collectionView: UICollectionView) { 129 | collectionView.register(CellType.classForCoder(), forCellWithReuseIdentifier: identifier) 130 | } 131 | } 132 | 133 | extension ItemType { 134 | func updateCell(_ cell: UICollectionViewCell) { fatalError() } 135 | } 136 | 137 | extension TypedItemType { 138 | func updateCell(_ cell: UICollectionViewCell) { 139 | guard let cell = cell as? CellType else { return } 140 | cell.update(with: self) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Sources/LegoKit/Core/LegoRenderer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LegoRenderer.swift 3 | // LegoKit 4 | // 5 | // Created by octree on 2022/3/11. 6 | // 7 | // Copyright (c) 2022 Octree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Combine 28 | import UIKit 29 | 30 | public typealias CellProvider = (UICollectionView, IndexPath, AnyHashable) -> UICollectionViewCell? 31 | public typealias SupplementaryViewProvider = (UICollectionView, String, IndexPath) -> UICollectionReusableView? 32 | 33 | @MainActor 34 | public final class LegoRenderer { 35 | public private(set) lazy var collectionView: UICollectionView = { 36 | let view = UICollectionView(frame: CGRect(), collectionViewLayout: layout) 37 | view.translatesAutoresizingMaskIntoConstraints = false 38 | return view 39 | }() 40 | 41 | private lazy var layout: UICollectionViewCompositionalLayout = { 42 | let provider: UICollectionViewCompositionalLayoutSectionProvider = { [unowned self] section, environment in 43 | lego.sections[section].layout(environment) 44 | } 45 | if let layoutConfiguration { 46 | return .init(sectionProvider: provider, configuration: layoutConfiguration) 47 | } else { 48 | return .init(sectionProvider: provider) 49 | } 50 | }() 51 | 52 | private lazy var dataSource: UICollectionViewDiffableDataSource = .init(collectionView: collectionView) { [weak self] in 53 | self?.cellProvider(collectionView: $0, indexPath: $1, itemID: $2) 54 | } 55 | 56 | public var supplementaryViewProvider: SupplementaryViewProvider? { 57 | get { 58 | dataSource.supplementaryViewProvider 59 | } 60 | set { 61 | dataSource.supplementaryViewProvider = newValue 62 | } 63 | } 64 | 65 | private var layoutConfiguration: UICollectionViewCompositionalLayoutConfiguration? 66 | public var isAnimationEnabled: Bool = true 67 | 68 | public private(set) var lego: Lego 69 | public init(lego: Lego, layoutConfiguration: UICollectionViewCompositionalLayoutConfiguration? = nil) { 70 | self.layoutConfiguration = layoutConfiguration 71 | self.lego = lego 72 | } 73 | 74 | public func render(in view: UIView, config: (UICollectionView) -> Void = { _ in }) { 75 | view.addSubview(collectionView) 76 | NSLayoutConstraint.activate([ 77 | collectionView.topAnchor.constraint(equalTo: view.topAnchor), 78 | collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 79 | collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 80 | collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor) 81 | ]) 82 | config(collectionView) 83 | apply(lego: lego, animatingDifferences: true) 84 | } 85 | 86 | /// Update collectionView with a specified ``Lego`` 87 | /// - Parameters: 88 | /// - lego: The lego specified to apply 89 | /// - animatingDifferences: A bool value indicates whether perform animations. 90 | public func apply(lego: Lego, animatingDifferences: Bool? = nil) { 91 | var snapshot = NSDiffableDataSourceSnapshot() 92 | snapshot.appendSections(lego.sections.map { $0.id }) 93 | lego.sections.forEach { 94 | snapshot.appendItems($0.items.map { $0.anyID }, toSection: $0.id) 95 | } 96 | let allIdentifiers = Set(snapshot.itemIdentifiers) 97 | for indexPath in collectionView.indexPathsForVisibleItems { 98 | guard let identifier = dataSource.itemIdentifier(for: indexPath), 99 | allIdentifiers.contains(identifier), 100 | let cell = collectionView.cellForItem(at: indexPath), 101 | let newIndexPath = lego.indexPathForItem(with: identifier) 102 | else { 103 | continue 104 | } 105 | 106 | lego[newIndexPath].updateCell(cell) 107 | } 108 | self.lego = lego 109 | dataSource.apply(snapshot, animatingDifferences: animatingDifferences ?? isAnimationEnabled) 110 | } 111 | 112 | private func cellProvider(collectionView: UICollectionView, indexPath: IndexPath, itemID: AnyHashable) -> UICollectionViewCell? { 113 | lego[indexPath].createCell(in: collectionView, at: indexPath) 114 | } 115 | } 116 | 117 | // MARK: - Subscript 118 | 119 | public extension LegoRenderer { 120 | subscript(indexPath: IndexPath) -> AnyItem { 121 | lego[indexPath] 122 | } 123 | 124 | subscript(indexPath: IndexPath, as type: C.Type) -> C? { 125 | lego[indexPath, as: type] 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Demo/Playground/Playground.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C202A26227D99B2000210384 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C202A26127D99B2000210384 /* AppDelegate.swift */; }; 11 | C202A26627D99B2000210384 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C202A26527D99B2000210384 /* ViewController.swift */; }; 12 | C202A26927D99B2000210384 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C202A26727D99B2000210384 /* Main.storyboard */; }; 13 | C202A26B27D99B2100210384 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C202A26A27D99B2100210384 /* Assets.xcassets */; }; 14 | C202A26E27D99B2100210384 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C202A26C27D99B2100210384 /* LaunchScreen.storyboard */; }; 15 | C26015E327D99C4000E63536 /* LegoKit in Frameworks */ = {isa = PBXBuildFile; productRef = C26015E227D99C4000E63536 /* LegoKit */; }; 16 | C26015E927DAF8C800E63536 /* GroupCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26015E827DAF8C800E63536 /* GroupCell.swift */; }; 17 | C26015EB27DAF8D000E63536 /* UserCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26015EA27DAF8D000E63536 /* UserCell.swift */; }; 18 | C26015F127DAFBF500E63536 /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26015F027DAFBF500E63536 /* Model.swift */; }; 19 | C26AE122283F78D200E65D2C /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = C26AE121283F78D200E65D2C /* SnapKit */; }; 20 | C290C3C327DB21A6005F0AEC /* LegoKit Documentation.docc in Sources */ = {isa = PBXBuildFile; fileRef = C290C3C227DB21A6005F0AEC /* LegoKit Documentation.docc */; }; 21 | C2FEB1F62823B616000FF4E5 /* ToggleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2FEB1F52823B616000FF4E5 /* ToggleViewController.swift */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | C202A25E27D99B2000210384 /* Playground.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Playground.app; sourceTree = BUILT_PRODUCTS_DIR; }; 26 | C202A26127D99B2000210384 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 27 | C202A26527D99B2000210384 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 28 | C202A26827D99B2000210384 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 29 | C202A26A27D99B2100210384 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 30 | C202A26D27D99B2100210384 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 31 | C202A26F27D99B2100210384 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 32 | C26015E427D99DFF00E63536 /* Playground.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Playground.entitlements; sourceTree = ""; }; 33 | C26015E827DAF8C800E63536 /* GroupCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCell.swift; sourceTree = ""; }; 34 | C26015EA27DAF8D000E63536 /* UserCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCell.swift; sourceTree = ""; }; 35 | C26015F027DAFBF500E63536 /* Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Model.swift; sourceTree = ""; }; 36 | C290C3C227DB21A6005F0AEC /* LegoKit Documentation.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = "LegoKit Documentation.docc"; sourceTree = ""; }; 37 | C2FEB1F52823B616000FF4E5 /* ToggleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleViewController.swift; sourceTree = ""; }; 38 | /* End PBXFileReference section */ 39 | 40 | /* Begin PBXFrameworksBuildPhase section */ 41 | C202A25B27D99B2000210384 /* Frameworks */ = { 42 | isa = PBXFrameworksBuildPhase; 43 | buildActionMask = 2147483647; 44 | files = ( 45 | C26015E327D99C4000E63536 /* LegoKit in Frameworks */, 46 | C26AE122283F78D200E65D2C /* SnapKit in Frameworks */, 47 | ); 48 | runOnlyForDeploymentPostprocessing = 0; 49 | }; 50 | /* End PBXFrameworksBuildPhase section */ 51 | 52 | /* Begin PBXGroup section */ 53 | C202A25527D99B2000210384 = { 54 | isa = PBXGroup; 55 | children = ( 56 | C290C3C227DB21A6005F0AEC /* LegoKit Documentation.docc */, 57 | C202A26027D99B2000210384 /* Playground */, 58 | C202A25F27D99B2000210384 /* Products */, 59 | C26015E127D99C4000E63536 /* Frameworks */, 60 | ); 61 | sourceTree = ""; 62 | }; 63 | C202A25F27D99B2000210384 /* Products */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | C202A25E27D99B2000210384 /* Playground.app */, 67 | ); 68 | name = Products; 69 | sourceTree = ""; 70 | }; 71 | C202A26027D99B2000210384 /* Playground */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | C26015ED27DAFBC800E63536 /* Resource */, 75 | C26015EE27DAFBCC00E63536 /* Source */, 76 | C26015E427D99DFF00E63536 /* Playground.entitlements */, 77 | C202A26F27D99B2100210384 /* Info.plist */, 78 | ); 79 | path = Playground; 80 | sourceTree = ""; 81 | }; 82 | C26015E127D99C4000E63536 /* Frameworks */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | ); 86 | name = Frameworks; 87 | sourceTree = ""; 88 | }; 89 | C26015E527D9AEDA00E63536 /* Item */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | C26015E827DAF8C800E63536 /* GroupCell.swift */, 93 | C26015EA27DAF8D000E63536 /* UserCell.swift */, 94 | ); 95 | path = Item; 96 | sourceTree = ""; 97 | }; 98 | C26015EC27DAFBC300E63536 /* Application */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | C202A26127D99B2000210384 /* AppDelegate.swift */, 102 | ); 103 | path = Application; 104 | sourceTree = ""; 105 | }; 106 | C26015ED27DAFBC800E63536 /* Resource */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | C202A26727D99B2000210384 /* Main.storyboard */, 110 | C202A26A27D99B2100210384 /* Assets.xcassets */, 111 | C202A26C27D99B2100210384 /* LaunchScreen.storyboard */, 112 | ); 113 | path = Resource; 114 | sourceTree = ""; 115 | }; 116 | C26015EE27DAFBCC00E63536 /* Source */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | C26015EC27DAFBC300E63536 /* Application */, 120 | C26015E527D9AEDA00E63536 /* Item */, 121 | C26015EF27DAFBEB00E63536 /* Model */, 122 | C202A26527D99B2000210384 /* ViewController.swift */, 123 | C2FEB1F52823B616000FF4E5 /* ToggleViewController.swift */, 124 | ); 125 | path = Source; 126 | sourceTree = ""; 127 | }; 128 | C26015EF27DAFBEB00E63536 /* Model */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | C26015F027DAFBF500E63536 /* Model.swift */, 132 | ); 133 | path = Model; 134 | sourceTree = ""; 135 | }; 136 | /* End PBXGroup section */ 137 | 138 | /* Begin PBXNativeTarget section */ 139 | C202A25D27D99B2000210384 /* Playground */ = { 140 | isa = PBXNativeTarget; 141 | buildConfigurationList = C202A27227D99B2100210384 /* Build configuration list for PBXNativeTarget "Playground" */; 142 | buildPhases = ( 143 | C202A25A27D99B2000210384 /* Sources */, 144 | C202A25B27D99B2000210384 /* Frameworks */, 145 | C202A25C27D99B2000210384 /* Resources */, 146 | C26AE11C283F74EA00E65D2C /* Swiftlint */, 147 | ); 148 | buildRules = ( 149 | ); 150 | dependencies = ( 151 | ); 152 | name = Playground; 153 | packageProductDependencies = ( 154 | C26015E227D99C4000E63536 /* LegoKit */, 155 | C26AE121283F78D200E65D2C /* SnapKit */, 156 | ); 157 | productName = Playground; 158 | productReference = C202A25E27D99B2000210384 /* Playground.app */; 159 | productType = "com.apple.product-type.application"; 160 | }; 161 | /* End PBXNativeTarget section */ 162 | 163 | /* Begin PBXProject section */ 164 | C202A25627D99B2000210384 /* Project object */ = { 165 | isa = PBXProject; 166 | attributes = { 167 | BuildIndependentTargetsInParallel = 1; 168 | LastSwiftUpdateCheck = 1320; 169 | LastUpgradeCheck = 1320; 170 | TargetAttributes = { 171 | C202A25D27D99B2000210384 = { 172 | CreatedOnToolsVersion = 13.2.1; 173 | }; 174 | }; 175 | }; 176 | buildConfigurationList = C202A25927D99B2000210384 /* Build configuration list for PBXProject "Playground" */; 177 | compatibilityVersion = "Xcode 13.0"; 178 | developmentRegion = en; 179 | hasScannedForEncodings = 0; 180 | knownRegions = ( 181 | en, 182 | Base, 183 | ); 184 | mainGroup = C202A25527D99B2000210384; 185 | packageReferences = ( 186 | C26AE120283F78D200E65D2C /* XCRemoteSwiftPackageReference "SnapKit" */, 187 | ); 188 | productRefGroup = C202A25F27D99B2000210384 /* Products */; 189 | projectDirPath = ""; 190 | projectRoot = ""; 191 | targets = ( 192 | C202A25D27D99B2000210384 /* Playground */, 193 | ); 194 | }; 195 | /* End PBXProject section */ 196 | 197 | /* Begin PBXResourcesBuildPhase section */ 198 | C202A25C27D99B2000210384 /* Resources */ = { 199 | isa = PBXResourcesBuildPhase; 200 | buildActionMask = 2147483647; 201 | files = ( 202 | C202A26E27D99B2100210384 /* LaunchScreen.storyboard in Resources */, 203 | C202A26B27D99B2100210384 /* Assets.xcassets in Resources */, 204 | C202A26927D99B2000210384 /* Main.storyboard in Resources */, 205 | ); 206 | runOnlyForDeploymentPostprocessing = 0; 207 | }; 208 | /* End PBXResourcesBuildPhase section */ 209 | 210 | /* Begin PBXShellScriptBuildPhase section */ 211 | C26AE11C283F74EA00E65D2C /* Swiftlint */ = { 212 | isa = PBXShellScriptBuildPhase; 213 | alwaysOutOfDate = 1; 214 | buildActionMask = 12; 215 | files = ( 216 | ); 217 | inputFileListPaths = ( 218 | ); 219 | inputPaths = ( 220 | ); 221 | name = Swiftlint; 222 | outputFileListPaths = ( 223 | ); 224 | outputPaths = ( 225 | ); 226 | runOnlyForDeploymentPostprocessing = 0; 227 | shellPath = /bin/sh; 228 | shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\nif which swiftlint >/dev/null; then\n cd \"${SRCROOT}/../..\" && swiftlint --config \"${SRCROOT}/../../.swiftlint.yml\" \nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n\n\n"; 229 | }; 230 | /* End PBXShellScriptBuildPhase section */ 231 | 232 | /* Begin PBXSourcesBuildPhase section */ 233 | C202A25A27D99B2000210384 /* Sources */ = { 234 | isa = PBXSourcesBuildPhase; 235 | buildActionMask = 2147483647; 236 | files = ( 237 | C26015F127DAFBF500E63536 /* Model.swift in Sources */, 238 | C26015E927DAF8C800E63536 /* GroupCell.swift in Sources */, 239 | C290C3C327DB21A6005F0AEC /* LegoKit Documentation.docc in Sources */, 240 | C202A26627D99B2000210384 /* ViewController.swift in Sources */, 241 | C26015EB27DAF8D000E63536 /* UserCell.swift in Sources */, 242 | C2FEB1F62823B616000FF4E5 /* ToggleViewController.swift in Sources */, 243 | C202A26227D99B2000210384 /* AppDelegate.swift in Sources */, 244 | ); 245 | runOnlyForDeploymentPostprocessing = 0; 246 | }; 247 | /* End PBXSourcesBuildPhase section */ 248 | 249 | /* Begin PBXVariantGroup section */ 250 | C202A26727D99B2000210384 /* Main.storyboard */ = { 251 | isa = PBXVariantGroup; 252 | children = ( 253 | C202A26827D99B2000210384 /* Base */, 254 | ); 255 | name = Main.storyboard; 256 | sourceTree = ""; 257 | }; 258 | C202A26C27D99B2100210384 /* LaunchScreen.storyboard */ = { 259 | isa = PBXVariantGroup; 260 | children = ( 261 | C202A26D27D99B2100210384 /* Base */, 262 | ); 263 | name = LaunchScreen.storyboard; 264 | sourceTree = ""; 265 | }; 266 | /* End PBXVariantGroup section */ 267 | 268 | /* Begin XCBuildConfiguration section */ 269 | C202A27027D99B2100210384 /* Debug */ = { 270 | isa = XCBuildConfiguration; 271 | buildSettings = { 272 | ALWAYS_SEARCH_USER_PATHS = NO; 273 | CLANG_ANALYZER_NONNULL = YES; 274 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 275 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 276 | CLANG_CXX_LIBRARY = "libc++"; 277 | CLANG_ENABLE_MODULES = YES; 278 | CLANG_ENABLE_OBJC_ARC = YES; 279 | CLANG_ENABLE_OBJC_WEAK = YES; 280 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 281 | CLANG_WARN_BOOL_CONVERSION = YES; 282 | CLANG_WARN_COMMA = YES; 283 | CLANG_WARN_CONSTANT_CONVERSION = YES; 284 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 285 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 286 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 287 | CLANG_WARN_EMPTY_BODY = YES; 288 | CLANG_WARN_ENUM_CONVERSION = YES; 289 | CLANG_WARN_INFINITE_RECURSION = YES; 290 | CLANG_WARN_INT_CONVERSION = YES; 291 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 292 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 293 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 294 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 295 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 296 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 297 | CLANG_WARN_STRICT_PROTOTYPES = YES; 298 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 299 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 300 | CLANG_WARN_UNREACHABLE_CODE = YES; 301 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 302 | COPY_PHASE_STRIP = NO; 303 | DEBUG_INFORMATION_FORMAT = dwarf; 304 | ENABLE_STRICT_OBJC_MSGSEND = YES; 305 | ENABLE_TESTABILITY = YES; 306 | GCC_C_LANGUAGE_STANDARD = gnu11; 307 | GCC_DYNAMIC_NO_PIC = NO; 308 | GCC_NO_COMMON_BLOCKS = YES; 309 | GCC_OPTIMIZATION_LEVEL = 0; 310 | GCC_PREPROCESSOR_DEFINITIONS = ( 311 | "DEBUG=1", 312 | "$(inherited)", 313 | ); 314 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 315 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 316 | GCC_WARN_UNDECLARED_SELECTOR = YES; 317 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 318 | GCC_WARN_UNUSED_FUNCTION = YES; 319 | GCC_WARN_UNUSED_VARIABLE = YES; 320 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 321 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 322 | MTL_FAST_MATH = YES; 323 | ONLY_ACTIVE_ARCH = YES; 324 | SDKROOT = iphoneos; 325 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 326 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 327 | }; 328 | name = Debug; 329 | }; 330 | C202A27127D99B2100210384 /* Release */ = { 331 | isa = XCBuildConfiguration; 332 | buildSettings = { 333 | ALWAYS_SEARCH_USER_PATHS = NO; 334 | CLANG_ANALYZER_NONNULL = YES; 335 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 336 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 337 | CLANG_CXX_LIBRARY = "libc++"; 338 | CLANG_ENABLE_MODULES = YES; 339 | CLANG_ENABLE_OBJC_ARC = YES; 340 | CLANG_ENABLE_OBJC_WEAK = YES; 341 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 342 | CLANG_WARN_BOOL_CONVERSION = YES; 343 | CLANG_WARN_COMMA = YES; 344 | CLANG_WARN_CONSTANT_CONVERSION = YES; 345 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 346 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 347 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 348 | CLANG_WARN_EMPTY_BODY = YES; 349 | CLANG_WARN_ENUM_CONVERSION = YES; 350 | CLANG_WARN_INFINITE_RECURSION = YES; 351 | CLANG_WARN_INT_CONVERSION = YES; 352 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 353 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 354 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 355 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 356 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 357 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 358 | CLANG_WARN_STRICT_PROTOTYPES = YES; 359 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 360 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 361 | CLANG_WARN_UNREACHABLE_CODE = YES; 362 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 363 | COPY_PHASE_STRIP = NO; 364 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 365 | ENABLE_NS_ASSERTIONS = NO; 366 | ENABLE_STRICT_OBJC_MSGSEND = YES; 367 | GCC_C_LANGUAGE_STANDARD = gnu11; 368 | GCC_NO_COMMON_BLOCKS = YES; 369 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 370 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 371 | GCC_WARN_UNDECLARED_SELECTOR = YES; 372 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 373 | GCC_WARN_UNUSED_FUNCTION = YES; 374 | GCC_WARN_UNUSED_VARIABLE = YES; 375 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 376 | MTL_ENABLE_DEBUG_INFO = NO; 377 | MTL_FAST_MATH = YES; 378 | SDKROOT = iphoneos; 379 | SWIFT_COMPILATION_MODE = wholemodule; 380 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 381 | VALIDATE_PRODUCT = YES; 382 | }; 383 | name = Release; 384 | }; 385 | C202A27327D99B2100210384 /* Debug */ = { 386 | isa = XCBuildConfiguration; 387 | buildSettings = { 388 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 389 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 390 | CODE_SIGN_ENTITLEMENTS = Playground/Playground.entitlements; 391 | CODE_SIGN_STYLE = Automatic; 392 | CURRENT_PROJECT_VERSION = 1; 393 | DEVELOPMENT_TEAM = 7A5GV3DH33; 394 | GENERATE_INFOPLIST_FILE = YES; 395 | INFOPLIST_FILE = Playground/Info.plist; 396 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 397 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 398 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 399 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 400 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 401 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 402 | LD_RUNPATH_SEARCH_PATHS = ( 403 | "$(inherited)", 404 | "@executable_path/Frameworks", 405 | ); 406 | MARKETING_VERSION = 1.0; 407 | PRODUCT_BUNDLE_IDENTIFIER = io.ijk.Playground; 408 | PRODUCT_NAME = "$(TARGET_NAME)"; 409 | SUPPORTS_MACCATALYST = YES; 410 | SWIFT_EMIT_LOC_STRINGS = YES; 411 | SWIFT_VERSION = 6.0; 412 | TARGETED_DEVICE_FAMILY = "1,2"; 413 | }; 414 | name = Debug; 415 | }; 416 | C202A27427D99B2100210384 /* Release */ = { 417 | isa = XCBuildConfiguration; 418 | buildSettings = { 419 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 420 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 421 | CODE_SIGN_ENTITLEMENTS = Playground/Playground.entitlements; 422 | CODE_SIGN_STYLE = Automatic; 423 | CURRENT_PROJECT_VERSION = 1; 424 | DEVELOPMENT_TEAM = 7A5GV3DH33; 425 | GENERATE_INFOPLIST_FILE = YES; 426 | INFOPLIST_FILE = Playground/Info.plist; 427 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 428 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 429 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 430 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 431 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 432 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 433 | LD_RUNPATH_SEARCH_PATHS = ( 434 | "$(inherited)", 435 | "@executable_path/Frameworks", 436 | ); 437 | MARKETING_VERSION = 1.0; 438 | PRODUCT_BUNDLE_IDENTIFIER = io.ijk.Playground; 439 | PRODUCT_NAME = "$(TARGET_NAME)"; 440 | SUPPORTS_MACCATALYST = YES; 441 | SWIFT_EMIT_LOC_STRINGS = YES; 442 | SWIFT_VERSION = 6.0; 443 | TARGETED_DEVICE_FAMILY = "1,2"; 444 | }; 445 | name = Release; 446 | }; 447 | /* End XCBuildConfiguration section */ 448 | 449 | /* Begin XCConfigurationList section */ 450 | C202A25927D99B2000210384 /* Build configuration list for PBXProject "Playground" */ = { 451 | isa = XCConfigurationList; 452 | buildConfigurations = ( 453 | C202A27027D99B2100210384 /* Debug */, 454 | C202A27127D99B2100210384 /* Release */, 455 | ); 456 | defaultConfigurationIsVisible = 0; 457 | defaultConfigurationName = Release; 458 | }; 459 | C202A27227D99B2100210384 /* Build configuration list for PBXNativeTarget "Playground" */ = { 460 | isa = XCConfigurationList; 461 | buildConfigurations = ( 462 | C202A27327D99B2100210384 /* Debug */, 463 | C202A27427D99B2100210384 /* Release */, 464 | ); 465 | defaultConfigurationIsVisible = 0; 466 | defaultConfigurationName = Release; 467 | }; 468 | /* End XCConfigurationList section */ 469 | 470 | /* Begin XCRemoteSwiftPackageReference section */ 471 | C26AE120283F78D200E65D2C /* XCRemoteSwiftPackageReference "SnapKit" */ = { 472 | isa = XCRemoteSwiftPackageReference; 473 | repositoryURL = "https://github.com/SnapKit/SnapKit"; 474 | requirement = { 475 | kind = upToNextMajorVersion; 476 | minimumVersion = 5.0.0; 477 | }; 478 | }; 479 | /* End XCRemoteSwiftPackageReference section */ 480 | 481 | /* Begin XCSwiftPackageProductDependency section */ 482 | C26015E227D99C4000E63536 /* LegoKit */ = { 483 | isa = XCSwiftPackageProductDependency; 484 | productName = LegoKit; 485 | }; 486 | C26AE121283F78D200E65D2C /* SnapKit */ = { 487 | isa = XCSwiftPackageProductDependency; 488 | package = C26AE120283F78D200E65D2C /* XCRemoteSwiftPackageReference "SnapKit" */; 489 | productName = SnapKit; 490 | }; 491 | /* End XCSwiftPackageProductDependency section */ 492 | }; 493 | rootObject = C202A25627D99B2000210384 /* Project object */; 494 | } 495 | --------------------------------------------------------------------------------