├── .gitignore
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ └── xcschemes
│ └── CompositionalLayoutSpreadsheet.xcscheme
├── Apr-05-2021 16-03-41.gif
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── CompositionalLayoutSpreadsheet
│ ├── CellData.swift
│ ├── CompositionalLayoutSpreadsheet.swift
│ ├── CompositionalSpreadsheetLayoutProvider.swift
│ ├── SectionData.swift
│ ├── StickyCellView.swift
│ ├── StickyColumnView.swift
│ └── ValueCell.swift
└── example
└── CompositionalLayoutSpreadsheetExample
├── CompositionalLayoutSpreadsheetExample.xcodeproj
├── project.pbxproj
└── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ ├── IDEWorkspaceChecks.plist
│ └── swiftpm
│ └── Package.resolved
└── CompositionalLayoutSpreadsheetExample
├── AppDelegate.swift
├── Assets.xcassets
├── AccentColor.colorset
│ └── Contents.json
├── AppIcon.appiconset
│ └── Contents.json
└── Contents.json
├── Base.lproj
├── LaunchScreen.storyboard
└── Main.storyboard
├── Info.plist
├── SceneDelegate.swift
└── ViewController.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | # Xcode
9 | #
10 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
11 |
12 | ## User settings
13 | xcuserdata/
14 |
15 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
16 | *.xcscmblueprint
17 | *.xccheckout
18 |
19 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
20 | build/
21 | DerivedData/
22 | *.moved-aside
23 | *.pbxuser
24 | !default.pbxuser
25 | *.mode1v3
26 | !default.mode1v3
27 | *.mode2v3
28 | !default.mode2v3
29 | *.perspectivev3
30 | !default.perspectivev3
31 |
32 | ## Obj-C/Swift specific
33 | *.hmap
34 |
35 | ## App packaging
36 | *.ipa
37 | *.dSYM.zip
38 | *.dSYM
39 |
40 | ## Playgrounds
41 | timeline.xctimeline
42 | playground.xcworkspace
43 |
44 | # Swift Package Manager
45 | #
46 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
47 | # Packages/
48 | # Package.pins
49 | # Package.resolved
50 | # *.xcodeproj
51 | #
52 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
53 | # hence it is not needed unless you have added a package configuration file to your project
54 | # .swiftpm
55 |
56 | .build/
57 |
58 | # CocoaPods
59 | #
60 | # We recommend against adding the Pods directory to your .gitignore. However
61 | # you should judge for yourself, the pros and cons are mentioned at:
62 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
63 | #
64 | # Pods/
65 | #
66 | # Add this line if you want to avoid checking in source code from the Xcode workspace
67 | # *.xcworkspace
68 |
69 | # Carthage
70 | #
71 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
72 | # Carthage/Checkouts
73 |
74 | Carthage/Build/
75 |
76 | # Accio dependency management
77 | Dependencies/
78 | .accio/
79 |
80 | # fastlane
81 | #
82 | # It is recommended to not store the screenshots in the git repo.
83 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
84 | # For more information about the recommended setup visit:
85 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
86 |
87 | fastlane/report.xml
88 | fastlane/Preview.html
89 | fastlane/screenshots/**/*.png
90 | fastlane/test_output
91 |
92 | # Code Injection
93 | #
94 | # After new code Injection tools there's a generated folder /iOSInjectionProject
95 | # https://github.com/johnno1962/injectionforxcode
96 |
97 | iOSInjectionProject/
98 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/CompositionalLayoutSpreadsheet.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
47 |
53 |
54 |
55 |
56 |
57 |
67 |
68 |
74 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/Apr-05-2021 16-03-41.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ericleiyang/CompositionalLayoutSpreadsheet/89040e5df26bb1bce4888559a3646e4ce8d0089f/Apr-05-2021 16-03-41.gif
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Eric
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
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: "CompositionalLayoutSpreadsheet",
8 | platforms: [
9 | .iOS(.v13)
10 | ],
11 | products: [
12 | // Products define the executables and libraries a package produces, and make them visible to other packages.
13 | .library(
14 | name: "CompositionalLayoutSpreadsheet",
15 | targets: ["CompositionalLayoutSpreadsheet"]),
16 | ],
17 | dependencies: [
18 | // Dependencies declare other packages that this package depends on.
19 | // .package(url: /* package url */, from: "1.0.0"),
20 | ],
21 | targets: [
22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
23 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
24 | .target(
25 | name: "CompositionalLayoutSpreadsheet",
26 | dependencies: [])
27 | ]
28 | )
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # CompositionalLayoutSpreadsheet
3 |
4 | [](http://cocoapods.org/pods/SwiftSpreadsheet)
5 | [](http://cocoapods.org/pods/SwiftSpreadsheet)
6 |
7 |
8 | ## Example
9 |
10 | To run the example project, clone the repo, and run `CompositionalLayoutSpreadsheetExample.xcodeproj` from the example directory.
11 |
12 | ## Requirements
13 |
14 | iOS 13.0 +
15 | Swift 5.0 +
16 |
17 | ## SPM Installation
18 |
19 | CompositionalLayoutSpreadsheet is available through Swift Package Manager. To install
20 | it, simply add the following line to your Podfile:
21 |
22 | ```swift
23 | dependencies: [
24 | .package(url: "https://github.com/ericleiyang/CompositionalLayoutSpreadsheet.git", .upToNextMajor(from: "1.0.0"))
25 | ]
26 | ```
27 | ## Demo
28 |
29 | 
30 |
31 | ## Features
32 |
33 | - [x] Spreadsheet like collection view
34 | - [x] Compositional collection view layout
35 | - [x] Sticky column at the left
36 | - [x] iPhone screen sizes supported
37 | - [x] iPad screen sizes supported
38 | - [x] Device rotation supported
39 | - [x] Custom sticky size width supported
40 | - [x] Custom value cell size supported
41 | - [x] Custom sticky cell font/color supported
42 | - [x] Custom value cell supported
43 | - [ ] Sticky row as the header
44 | - [ ] Multiple sticky columns at the left
45 | - [ ] Multiple sticky rows as the header
46 | - [ ] Various row sizes supported
47 | - [ ] Various column sizes supported
48 |
49 | ## Quick start
50 |
51 | A short introduction on how to get started:
52 |
53 | - The sticky column is represented as `ReusableSupplementaryView`.
54 | - The values are represented as one `NSCollectionLayoutSection` with multiple `NSCollectionLayoutGroup`.
55 | - Default cell is the `ValueCell` with one label displayed. It can be replaced by custom `UICollectionViewCell` when calling `configureHierarchy` of the `CompositionalLayoutSpreadsheet`.
56 |
57 | A short example:
58 |
59 | ```swift
60 | //Init
61 | let provider = CompositionalLayoutSpreadsheet()
62 |
63 | // Use default ValueCell
64 | provider.configureHierarchy(
65 | stikyColumnDatas: stikyColumnDatas,
66 | rowDatas: rowDatas,
67 | parentView: view
68 | )
69 | ```
70 |
71 | To use the custom cell:
72 |
73 | ```swift
74 | //Init
75 | let provider = CompositionalLayoutSpreadsheet()
76 |
77 | // Use default ValueCell
78 | provider.configureHierarchy(
79 | stikyColumnDatas: stikyColumnDatas,
80 | rowDatas: rowDatas,
81 | cell: YOUR CELL,
82 | cellReuseIdentifier: The reuse identifier of YOUR CELL
83 | )
84 | ```
85 |
86 |
87 | Reload after data updated:
88 | ```swift
89 | provider.update(
90 | stikyColumnDatas: UPDATED COLUMN DATA,
91 | rowDatas: UPDATED VALUES
92 | )
93 | ```
94 |
95 | Enjoy ;)
96 |
97 | ## Questions
98 |
99 | Please refer to the demo application or contact me directly.
100 |
101 | ## Author
102 |
103 | Eric Yang
104 |
105 | ## License
106 |
107 | CompositionalLayoutSpreadsheet is available under the MIT license. See the LICENSE file for more info.
108 |
--------------------------------------------------------------------------------
/Sources/CompositionalLayoutSpreadsheet/CellData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CellData.swift
3 | //
4 | //
5 | // Created by Eric Yang on 5/4/21.
6 | //
7 |
8 | import Foundation
9 |
10 | class CellData {
11 | let attributedText: NSAttributedString
12 | let identifier: String
13 |
14 | init(
15 | attributedText: NSAttributedString,
16 | identifier: String = UUID().uuidString
17 | ) {
18 | self.attributedText = attributedText
19 | self.identifier = identifier
20 | }
21 | }
22 |
23 | extension CellData: Hashable {
24 | func hash(into hasher: inout Hasher) {
25 | hasher.combine(identifier)
26 | }
27 |
28 | static func == (lhs: CellData, rhs: CellData) -> Bool {
29 | return lhs.identifier == rhs.identifier
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/CompositionalLayoutSpreadsheet/CompositionalLayoutSpreadsheet.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompositionalLayoutSpreadsheet.swift
3 | //
4 | //
5 | // Created by Eric Yang on 5/4/21.
6 | //
7 |
8 | import UIKit
9 |
10 | public class CompositionalLayoutSpreadsheet {
11 | // MARK: DATA
12 |
13 | /**
14 | -----------------------------------
15 | 0 |
16 | -----------------------------------
17 | 1 |
18 | -----------------------------------
19 | 2 |
20 | -----------------------------------
21 | ...
22 |
23 | sticky column datas = [0, 1, 2, ...]
24 | */
25 | private var stikyColumnDatas: [NSAttributedString] = [] {
26 | didSet {
27 | stickyColumnCellDatas = stikyColumnDatas.compactMap{ CellData(attributedText: $0) }
28 | }
29 | }
30 | private var stickyColumnCellDatas: [CellData] = []
31 | /**
32 | -----------------------------------
33 | 0 | 00 | 01 | 02 | ...
34 | -----------------------------------
35 | 1 | 10 | 11 | 12 | ...
36 | -----------------------------------
37 | ...
38 | row datas = [
39 | [00, 01, 02, ...],
40 | [10, 11, 12, ...],
41 | ...
42 | ]
43 | */
44 | private var rowDatas: [[NSAttributedString]] = [] {
45 | didSet {
46 | let cellDatas = rowDatas.flatMap{ $0 }.map{ CellData(attributedText: $0) }
47 | sectionData = SectionData(cells: cellDatas)
48 | }
49 | }
50 | private var sectionData: SectionData?
51 |
52 | // MARK: UI
53 |
54 | let spreadsheetBackgroundColor: UIColor
55 | let cellWidth: CGFloat
56 | let cellHeight: CGFloat
57 | let cellBorderWidth: CGFloat
58 | let cellBorderColor: UIColor
59 | let cellBackgroundColor: UIColor
60 | let stickyCellBackgroundColor: UIColor
61 |
62 | public init(
63 | spreadsheetBackgroundColor: UIColor = .white,
64 | cellWidth: CGFloat = 100,
65 | cellHeight: CGFloat = 44,
66 | cellBorderWidth: CGFloat = 1,
67 | cellBorderColor: UIColor = .darkGray,
68 | cellBackgroundColor: UIColor = .white,
69 | stickyCellBackgroundColor: UIColor = .lightGray
70 | ) {
71 | self.spreadsheetBackgroundColor = spreadsheetBackgroundColor
72 | self.cellWidth = cellWidth
73 | self.cellHeight = cellHeight
74 | self.cellBorderWidth = cellBorderWidth
75 | self.cellBorderColor = cellBorderColor
76 | self.cellBackgroundColor = cellBackgroundColor
77 | self.stickyCellBackgroundColor = stickyCellBackgroundColor
78 | }
79 |
80 | private var parentView: UIView?
81 | private var cell: UICollectionViewCell.Type?
82 | private var cellReuseIdentifier: String?
83 |
84 | // update data
85 | public func update(
86 | stikyColumnDatas: [NSAttributedString],
87 | rowDatas: [[NSAttributedString]]
88 | ) {
89 | guard let parentView = parentView,
90 | let cell = cell,
91 | let cellReuseIdentifier = cellReuseIdentifier else {
92 | return
93 | }
94 | configureHierarchy(
95 | stikyColumnDatas: stikyColumnDatas,
96 | rowDatas: rowDatas,
97 | parentView: parentView,
98 | cell: cell,
99 | cellReuseIdentifier: cellReuseIdentifier
100 | )
101 | }
102 |
103 | public func configureHierarchy(
104 | stikyColumnDatas: [NSAttributedString],
105 | rowDatas: [[NSAttributedString]],
106 | parentView: UIView,
107 | cell: UICollectionViewCell.Type = ValueCell.self,
108 | cellReuseIdentifier: String = ValueCell.reuseIdentifier
109 | ) {
110 | self.stikyColumnDatas = stikyColumnDatas
111 | self.rowDatas = rowDatas
112 | if let collectionView = collectionView {
113 | collectionView.removeFromSuperview()
114 | }
115 | collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout())
116 | collectionView.register(
117 | cell,
118 | forCellWithReuseIdentifier: cellReuseIdentifier
119 | )
120 | collectionView.register(
121 | StickColumnView.self,
122 | forSupplementaryViewOfKind: StickColumnView.reuseElementKind,
123 | withReuseIdentifier: StickColumnView.reuseIdentifier
124 | )
125 | collectionView.translatesAutoresizingMaskIntoConstraints = false
126 | collectionView.backgroundColor = spreadsheetBackgroundColor
127 | collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
128 | parentView.addSubview(collectionView)
129 | NSLayoutConstraint.activate([
130 | collectionView.leadingAnchor.constraint(equalTo: parentView.safeAreaLayoutGuide.leadingAnchor),
131 | collectionView.trailingAnchor.constraint(equalTo: parentView.safeAreaLayoutGuide.trailingAnchor),
132 | collectionView.topAnchor.constraint(equalTo: parentView.safeAreaLayoutGuide.topAnchor),
133 | collectionView.bottomAnchor.constraint(equalTo: parentView.safeAreaLayoutGuide.bottomAnchor)
134 | ])
135 | configureDataSource()
136 | updateDataSource()
137 | }
138 |
139 | // MARK: UICollectionView
140 | private var collectionView: UICollectionView!
141 | private var dataSource: UICollectionViewDiffableDataSource! = nil
142 | private var layoutProvider: CompositionalSpreadsheetLayoutProvider!
143 |
144 | private func createLayout() -> UICollectionViewCompositionalLayout {
145 | layoutProvider = CompositionalSpreadsheetLayoutProvider(
146 | cellWidth: cellWidth,
147 | cellHeight: cellHeight,
148 | numberOfDataRows: rowDatas.count,
149 | numberOfDataColumns: rowDatas.first?.count ?? 0,
150 | stickyColumnElementKind: StickColumnView.reuseElementKind
151 | )
152 | return layoutProvider.layout
153 | }
154 |
155 | func configureDataSource() {
156 | dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {
157 | (collectionView: UICollectionView, indexPath: IndexPath, data: CellData) -> UICollectionViewCell? in
158 | // Return the cell.
159 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ValueCell.reuseIdentifier, for: indexPath) as? ValueCell else {
160 | return nil
161 | }
162 | cell.configure(
163 | attributedText: data.attributedText,
164 | backgroundColor: self.cellBackgroundColor,
165 | borderWith: self.cellBorderWidth,
166 | borderColor: self.cellBorderColor
167 | )
168 | return cell
169 | }
170 |
171 | dataSource.supplementaryViewProvider = {(
172 | collectionView: UICollectionView,
173 | kind: String,
174 | indexPath: IndexPath
175 | ) -> UICollectionReusableView? in
176 | guard let stickColumnView = collectionView.dequeueReusableSupplementaryView(
177 | ofKind: StickColumnView.reuseElementKind,
178 | withReuseIdentifier: StickColumnView.reuseIdentifier,
179 | for: indexPath
180 | ) as? StickColumnView else {
181 | fatalError("Cannot create new supplementary")
182 | }
183 | stickColumnView.configure(
184 | stickyColumnDatas: self.stickyColumnCellDatas,
185 | stickyColumnWidth: self.cellWidth,
186 | stickyCellHeight: self.cellHeight,
187 | stickyCellBackgroundColor: self.stickyCellBackgroundColor,
188 | stickyCellBorderWidth: self.cellBorderWidth,
189 | stickyCellBorderColor: self.cellBorderColor
190 | )
191 | return stickColumnView
192 | }
193 | }
194 |
195 | func updateDataSource() {
196 | guard let sectionData = sectionData else { return }
197 | var snapshot = NSDiffableDataSourceSnapshot()
198 | snapshot.appendSections([sectionData])
199 | snapshot.appendItems(sectionData.cells)
200 | dataSource.apply(snapshot, animatingDifferences: false)
201 | }
202 | }
203 |
204 |
--------------------------------------------------------------------------------
/Sources/CompositionalLayoutSpreadsheet/CompositionalSpreadsheetLayoutProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompositionalSpreadsheetLayoutProvider.swift
3 | //
4 | //
5 | // Created by Eric Yang on 5/4/21.
6 | //
7 |
8 | import UIKit
9 |
10 | class CompositionalSpreadsheetLayoutProvider {
11 | let cellWidth: CGFloat
12 | let cellHeight: CGFloat
13 | let numberOfDataRows: CGFloat
14 | let numberOfDataColumns: CGFloat
15 | let stickyColumnElementKind: String
16 |
17 | private var columnHeight: CGFloat {
18 | return numberOfDataRows * cellHeight
19 | }
20 |
21 | private var contentWidth: CGFloat {
22 | return numberOfDataColumns * cellWidth
23 | }
24 |
25 | private var scrollingWidth: CGFloat {
26 | return contentWidth - 2 * cellWidth
27 | }
28 |
29 | required init(
30 | cellWidth: CGFloat,
31 | cellHeight: CGFloat,
32 | numberOfDataRows: Int,
33 | numberOfDataColumns: Int,
34 | stickyColumnElementKind: String
35 | ) {
36 | self.cellWidth = cellWidth
37 | self.cellHeight = cellHeight
38 | self.numberOfDataRows = CGFloat(numberOfDataRows)
39 | self.numberOfDataColumns = CGFloat(numberOfDataColumns)
40 | self.stickyColumnElementKind = stickyColumnElementKind
41 | }
42 |
43 | var layout: UICollectionViewCompositionalLayout {
44 | // Sticky column
45 | let stickyColumnCellSize = NSCollectionLayoutSize(
46 | widthDimension: .absolute(cellWidth),
47 | heightDimension: .absolute(columnHeight)
48 | )
49 | let stickyColumn = NSCollectionLayoutBoundarySupplementaryItem(
50 | layoutSize: stickyColumnCellSize,
51 | elementKind: stickyColumnElementKind,
52 | alignment: .leading,
53 | absoluteOffset: CGPoint(x: -cellWidth, y: 0)
54 | )
55 | stickyColumn.pinToVisibleBounds = true
56 | stickyColumn.zIndex = 2
57 |
58 | // Item cell
59 | let itemSize = NSCollectionLayoutSize(
60 | widthDimension: .absolute(cellWidth),
61 | heightDimension: .absolute(cellHeight)
62 | )
63 | let item = NSCollectionLayoutItem(layoutSize: itemSize)
64 | let horizontalGroupSize = NSCollectionLayoutSize(
65 | widthDimension: .absolute(cellWidth * CGFloat(numberOfDataColumns)),
66 | heightDimension: .absolute(cellHeight)
67 | )
68 | var groups: [NSCollectionLayoutGroup] = []
69 | for _ in 0.. Bool {
29 | return lhs.identifier == rhs.identifier
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/CompositionalLayoutSpreadsheet/StickyCellView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StickyCellView.swift
3 | //
4 | //
5 | // Created by Eric Yang on 5/4/21.
6 | //
7 |
8 | import UIKit
9 |
10 | class StickyCellView: UIView {
11 | let label = UILabel()
12 |
13 | override init(frame: CGRect) {
14 | super.init(frame: frame)
15 | }
16 |
17 | required init?(coder: NSCoder) {
18 | fatalError("Not implemented")
19 | }
20 |
21 | func configure(
22 | attributedText: NSAttributedString,
23 | backgroundColor: UIColor,
24 | borderWith: CGFloat,
25 | borderColor: UIColor
26 | ) {
27 | self.backgroundColor = backgroundColor
28 | layer.borderWidth = borderWith
29 | layer.borderColor = borderColor.cgColor
30 | layer.masksToBounds = true
31 |
32 | label.translatesAutoresizingMaskIntoConstraints = false
33 | label.adjustsFontForContentSizeCategory = true
34 | addSubview(label)
35 | NSLayoutConstraint.activate([
36 | label.leadingAnchor.constraint(equalTo: leadingAnchor),
37 | label.trailingAnchor.constraint(equalTo: trailingAnchor),
38 | label.topAnchor.constraint(equalTo: topAnchor),
39 | label.bottomAnchor.constraint(equalTo: bottomAnchor)
40 | ])
41 | label.attributedText = attributedText
42 | label.textAlignment = .center
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/CompositionalLayoutSpreadsheet/StickyColumnView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StickColumnView.swift
3 | //
4 | //
5 | // Created by Eric Yang on 5/4/21.
6 | //
7 |
8 | import UIKit
9 |
10 | class StickColumnView: UICollectionReusableView {
11 |
12 | static let reuseIdentifier = "sticky-column-reuse-identifier"
13 | static let reuseElementKind = "sticky-column-element-kind"
14 |
15 | override init(frame: CGRect) {
16 | super.init(frame: frame)
17 | }
18 |
19 | required init?(coder: NSCoder) {
20 | fatalError("Not implemented")
21 | }
22 |
23 | func configure(
24 | stickyColumnDatas: [CellData],
25 | stickyColumnWidth: CGFloat,
26 | stickyCellHeight: CGFloat,
27 | stickyCellBackgroundColor: UIColor,
28 | stickyCellBorderWidth: CGFloat,
29 | stickyCellBorderColor: UIColor
30 | ) {
31 | for index in 0..
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/CompositionalLayoutSpreadsheetExample/CompositionalLayoutSpreadsheetExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/CompositionalLayoutSpreadsheetExample/CompositionalLayoutSpreadsheetExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "CompositionalLayoutSpreadsheet",
6 | "repositoryURL": "https://github.com/ericleiyang/CompositionalLayoutSpreadsheet",
7 | "state": {
8 | "branch": null,
9 | "revision": "89034315094fc8592feb1838613ff63684e6fc87",
10 | "version": "1.0.2"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/example/CompositionalLayoutSpreadsheetExample/CompositionalLayoutSpreadsheetExample/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // CompositionalLayoutSpreadsheetExample
4 | //
5 | // Created by Eric Yang on 3/4/21.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 |
14 |
15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
16 | // Override point for customization after application launch.
17 | return true
18 | }
19 |
20 | // MARK: UISceneSession Lifecycle
21 |
22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
23 | // Called when a new scene session is being created.
24 | // Use this method to select a configuration to create the new scene with.
25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
26 | }
27 |
28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
29 | // Called when the user discards a scene session.
30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
32 | }
33 |
34 |
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/example/CompositionalLayoutSpreadsheetExample/CompositionalLayoutSpreadsheetExample/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 |
--------------------------------------------------------------------------------
/example/CompositionalLayoutSpreadsheetExample/CompositionalLayoutSpreadsheetExample/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 |
--------------------------------------------------------------------------------
/example/CompositionalLayoutSpreadsheetExample/CompositionalLayoutSpreadsheetExample/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/example/CompositionalLayoutSpreadsheetExample/CompositionalLayoutSpreadsheetExample/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 |
--------------------------------------------------------------------------------
/example/CompositionalLayoutSpreadsheetExample/CompositionalLayoutSpreadsheetExample/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 |
--------------------------------------------------------------------------------
/example/CompositionalLayoutSpreadsheetExample/CompositionalLayoutSpreadsheetExample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UISceneConfigurationName
33 | Default Configuration
34 | UISceneDelegateClassName
35 | $(PRODUCT_MODULE_NAME).SceneDelegate
36 | UISceneStoryboardFile
37 | Main
38 |
39 |
40 |
41 |
42 | UIApplicationSupportsIndirectInputEvents
43 |
44 | UILaunchStoryboardName
45 | LaunchScreen
46 | UIMainStoryboardFile
47 | Main
48 | UIRequiredDeviceCapabilities
49 |
50 | armv7
51 |
52 | UISupportedInterfaceOrientations
53 |
54 | UIInterfaceOrientationPortrait
55 | UIInterfaceOrientationLandscapeLeft
56 | UIInterfaceOrientationLandscapeRight
57 |
58 | UISupportedInterfaceOrientations~ipad
59 |
60 | UIInterfaceOrientationPortrait
61 | UIInterfaceOrientationPortraitUpsideDown
62 | UIInterfaceOrientationLandscapeLeft
63 | UIInterfaceOrientationLandscapeRight
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/example/CompositionalLayoutSpreadsheetExample/CompositionalLayoutSpreadsheetExample/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // CompositionalLayoutSpreadsheetExample
4 | //
5 | // Created by Eric Yang on 3/4/21.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 |
15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
19 | guard let _ = (scene as? UIWindowScene) else { return }
20 | }
21 |
22 | func sceneDidDisconnect(_ scene: UIScene) {
23 | // Called as the scene is being released by the system.
24 | // This occurs shortly after the scene enters the background, or when its session is discarded.
25 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
27 | }
28 |
29 | func sceneDidBecomeActive(_ scene: UIScene) {
30 | // Called when the scene has moved from an inactive state to an active state.
31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
32 | }
33 |
34 | func sceneWillResignActive(_ scene: UIScene) {
35 | // Called when the scene will move from an active state to an inactive state.
36 | // This may occur due to temporary interruptions (ex. an incoming phone call).
37 | }
38 |
39 | func sceneWillEnterForeground(_ scene: UIScene) {
40 | // Called as the scene transitions from the background to the foreground.
41 | // Use this method to undo the changes made on entering the background.
42 | }
43 |
44 | func sceneDidEnterBackground(_ scene: UIScene) {
45 | // Called as the scene transitions from the foreground to the background.
46 | // Use this method to save data, release shared resources, and store enough scene-specific state information
47 | // to restore the scene back to its current state.
48 | }
49 |
50 |
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/example/CompositionalLayoutSpreadsheetExample/CompositionalLayoutSpreadsheetExample/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // CompositionalLayoutSpreadsheetExample
4 | //
5 | // Created by Eric Yang on 3/4/21.
6 | //
7 |
8 | import CompositionalLayoutSpreadsheet
9 | import UIKit
10 |
11 | class ViewController: UIViewController {
12 | var provider: CompositionalLayoutSpreadsheet!
13 | override func viewDidLoad() {
14 | super.viewDidLoad()
15 | navigationItem.title = "Compositional Spreadsheet"
16 | provider = CompositionalLayoutSpreadsheet()
17 | provider.configureHierarchy(
18 | stikyColumnDatas: stikyColumnDatas,
19 | rowDatas: rowDatas,
20 | parentView: view
21 | )
22 | }
23 | }
24 |
25 | extension ViewController {
26 | var stikyColumnDatas: [NSAttributedString] {
27 | var data: [NSAttributedString] = []
28 | for index in 0..<50 {
29 | let attributedText = NSAttributedString(
30 | string: "row \(index)",
31 | attributes: [
32 | .font : UIFont.systemFont(ofSize: 16),
33 | .foregroundColor: UIColor.black
34 | ]
35 | )
36 | data.append(attributedText)
37 | }
38 | return data
39 | }
40 | var rowDatas: [[NSAttributedString]] {
41 | var dataCollections: [[NSAttributedString]] = []
42 |
43 | for row in 0..<50 {
44 | var data: [NSAttributedString] = []
45 | for column in 0..<20 {
46 | let attributedText = NSAttributedString(
47 | string: "data \(row),\(column)",
48 | attributes: [
49 | .font : UIFont.systemFont(ofSize: 16),
50 | .foregroundColor: UIColor.lightGray
51 | ]
52 | )
53 | data.append(attributedText)
54 | }
55 | dataCollections.append(data)
56 | }
57 | return dataCollections
58 | }
59 | }
60 |
--------------------------------------------------------------------------------