├── Source
├── .gitkeep
├── Cells
│ ├── DataGridViewCornerHeaderCell.swift
│ ├── DataGridViewContentCell.swift
│ ├── DataGridViewRowHeaderCell.swift
│ ├── DataGridViewColumnHeaderCell.swift
│ ├── DataGridViewBaseCell.swift
│ └── DataGridViewBaseHeaderCell.swift
├── Utility
│ ├── UIView+Appearance_Swift.h
│ ├── UIView+Appearance_Swift.m
│ ├── UILabel+AppearanceSelectors.m
│ ├── IndexPath+DataGrid.swift
│ ├── UILabel+AppearanceSelectors.h
│ └── BorderHelper.swift
├── CollectionViewDelegate.swift
├── CollectionViewDataSource.swift
├── DataGridViewLayout.swift
└── DataGridView.swift
├── Pod
└── Assets
│ └── .gitkeep
├── _Pods.xcodeproj
├── Example
├── screenshot_01.png
├── screenshot_02.png
├── Gemfile
├── GlyuckDataGrid
│ ├── Examples
│ │ ├── ObjectiveC
│ │ │ ├── GlyuckDataGrid_Example-Bridging-Header.h
│ │ │ ├── ObjectiveCDataGridViewController.h
│ │ │ └── ObjectiveCDataGridViewController.m
│ │ ├── SpreadSheet
│ │ │ ├── Views
│ │ │ │ ├── SpreadSheetCell.swift
│ │ │ │ └── SpreadSheetCell.xib
│ │ │ └── SpreadSheetViewController.swift
│ │ ├── SimpleDataGrid
│ │ │ └── SimpleDataGridViewController.swift
│ │ └── MultiplicationTable
│ │ │ └── MultiplicationTableViewController.swift
│ ├── AppDelegate.swift
│ ├── Images.xcassets
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Info.plist
│ ├── Base.lproj
│ │ ├── LaunchScreen.xib
│ │ └── Main.storyboard
│ └── F1DataSource.swift
├── GlyuckDataGrid.xcodeproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── GlyuckDataGrid-Example.xcscheme
├── GlyuckDataGrid.xcworkspace
│ └── contents.xcworkspacedata
├── Podfile
├── Podfile.lock
├── Tests
│ ├── Utility
│ │ ├── NSIndexPath+DataGridSpec.swift
│ │ └── BorderHelperSpec.swift
│ ├── Info.plist
│ ├── Cells
│ │ ├── DataGridViewBaseCellSpec.swift
│ │ └── DataGridViewBaseHeaderCellSpec.swift
│ ├── TestDoubles
│ │ ├── StubDataGridViewDelegate.swift
│ │ └── StubDataGridViewDataSource.swift
│ ├── CollectionViewDelegateSpec.swift
│ ├── CollectionViewDataSourceSpec.swift
│ └── DataGridViewSpec.swift
└── Gemfile.lock
├── CHANGELOG.md
├── .gitignore
├── .travis.yml
├── GlyuckDataGrid.podspec
├── LICENSE
└── README.md
/Source/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Pod/Assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/_Pods.xcodeproj:
--------------------------------------------------------------------------------
1 | Example/Pods/Pods.xcodeproj
--------------------------------------------------------------------------------
/Example/screenshot_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glyuck/GlyuckDataGrid/HEAD/Example/screenshot_01.png
--------------------------------------------------------------------------------
/Example/screenshot_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glyuck/GlyuckDataGrid/HEAD/Example/screenshot_02.png
--------------------------------------------------------------------------------
/Example/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'cocoapods', '1.1.0.rc.2'
4 | gem 'activesupport', '~> 4.2.7'
5 |
--------------------------------------------------------------------------------
/Example/GlyuckDataGrid/Examples/ObjectiveC/GlyuckDataGrid_Example-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 |
5 | #import "ObjectiveCDataGridViewController.h"
6 |
--------------------------------------------------------------------------------
/Example/GlyuckDataGrid.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Source/Cells/DataGridViewCornerHeaderCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataGridViewCornerHeaderCell.swift
3 | // Pods
4 | //
5 | // Created by Vladimir Lyukov on 23/11/15.
6 | //
7 | //
8 |
9 | import UIKit
10 |
11 |
12 | open class DataGridViewCornerHeaderCell: DataGridViewBaseHeaderCell {
13 | }
14 |
--------------------------------------------------------------------------------
/Example/GlyuckDataGrid.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## 0.3.0 (2016-11-02)
4 |
5 | * Swift 3.0 compatibility
6 |
7 | ## 0.2.1 (2016-09-22)
8 |
9 | * Fixed crash when tapping row header
10 |
11 | ## 0.2.0 (2016-06-07)
12 |
13 | * Fixed crash when there are 0 items in data grid
14 | * Swift 2.2 compatibility
15 |
16 | ## 0.1.0 (2015-11-24)
17 |
18 | Initial release
19 |
--------------------------------------------------------------------------------
/Example/GlyuckDataGrid/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // GlyuckDataGrid
4 | //
5 | // Created by Vladimir Lyukov on 07/30/2015.
6 | // Copyright (c) 2015 Vladimir Lyukov. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 | var window: UIWindow?
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/Example/Podfile:
--------------------------------------------------------------------------------
1 | source 'https://github.com/CocoaPods/Specs.git'
2 | use_frameworks!
3 |
4 | target 'GlyuckDataGrid_Example' do
5 | pod "GlyuckDataGrid", :path => "../"
6 | end
7 |
8 | target 'GlyuckDataGrid_Tests' do
9 | pod "GlyuckDataGrid", :path => "../"
10 |
11 | pod 'Quick'
12 | pod 'Nimble'
13 | # pod 'FBSnapshotTestCase'
14 | # pod 'Nimble-Snapshots'
15 | end
16 |
--------------------------------------------------------------------------------
/Example/GlyuckDataGrid/Examples/ObjectiveC/ObjectiveCDataGridViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // ObjectiveCDataGridViewController.h
3 | // GlyuckDataGrid
4 | //
5 | // Created by Vladimir Lyukov on 24/11/2016.
6 | // Copyright © 2016 CocoaPods. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 |
12 | @interface ObjectiveCDataGridViewController : UIViewController
13 |
14 | @end
15 |
--------------------------------------------------------------------------------
/Example/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - GlyuckDataGrid (0.2.1)
3 | - Nimble (5.0.0)
4 | - Quick (0.10.0)
5 |
6 | DEPENDENCIES:
7 | - GlyuckDataGrid (from `../`)
8 | - Nimble
9 | - Quick
10 |
11 | EXTERNAL SOURCES:
12 | GlyuckDataGrid:
13 | :path: ../
14 |
15 | SPEC CHECKSUMS:
16 | GlyuckDataGrid: 975d0756b74c241c3be16abe20de5b82999cc945
17 | Nimble: 56fc9f5020effa2206de22c3dd910f4fb011b92f
18 | Quick: 5d290df1c69d5ee2f0729956dcf0fd9a30447eaa
19 |
20 | PODFILE CHECKSUM: e21efb884f3fce871cdbb16508cfe8ad6cd2c1b3
21 |
22 | COCOAPODS: 1.1.0.rc.2
23 |
--------------------------------------------------------------------------------
/Source/Utility/UIView+Appearance_Swift.h:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+Appearance_Swift.h
3 | // Pods
4 | //
5 | // Created by Vladimir Lyukov on 14/08/15.
6 | //
7 | //
8 |
9 | #import
10 |
11 | @interface UIView (Appearance_Swift)
12 |
13 | // appearanceWhenContainedIn: is not available in Swift. This fixes that.
14 | + (instancetype)glyuck_appearanceWhenContainedIn:(Class)containerClass;
15 | + (instancetype)glyuck_appearanceWhenContainedIn:(Class)containerClass class2:(Class)containerClass2;
16 |
17 | @end
18 |
--------------------------------------------------------------------------------
/Example/Tests/Utility/NSIndexPath+DataGridSpec.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSIndexPath+DataGridSpec.swift
3 | //
4 | // Created by Vladimir Lyukov on 31/07/15.
5 | //
6 |
7 | import Quick
8 | import Nimble
9 | import GlyuckDataGrid
10 |
11 |
12 | class NSIndexPathDataGridSpec: QuickSpec {
13 | override func spec() {
14 | describe("NSIndexPath") {
15 | it("can be initialized with column, row") {
16 | let indexPath = IndexPath(forColumn: 1, row: 2)
17 | expect(indexPath.dataGridColumn) == 1
18 | expect(indexPath.dataGridRow) == 2
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Source/Utility/UIView+Appearance_Swift.m:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+Appearance_Swift.m
3 | // Pods
4 | //
5 | // Created by Vladimir Lyukov on 14/08/15.
6 | //
7 | //
8 |
9 | #import "UIView+Appearance_Swift.h"
10 |
11 | @implementation UIView (Appearance_Swift)
12 |
13 | + (instancetype)glyuck_appearanceWhenContainedIn:(Class)containerClass {
14 | return [self appearanceWhenContainedIn:containerClass, nil];
15 | }
16 |
17 | + (instancetype)glyuck_appearanceWhenContainedIn:(Class)containerClass class2:(Class)containerClass2 {
18 | return [self appearanceWhenContainedIn:containerClass, containerClass2, nil];
19 | }
20 |
21 | @end
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS X
2 | .DS_Store
3 |
4 | # Xcode
5 | build/
6 | *.pbxuser
7 | !default.pbxuser
8 | *.mode1v3
9 | !default.mode1v3
10 | *.mode2v3
11 | !default.mode2v3
12 | *.perspectivev3
13 | !default.perspectivev3
14 | xcuserdata
15 | *.xccheckout
16 | profile
17 | *.moved-aside
18 | DerivedData
19 | *.hmap
20 | *.ipa
21 |
22 | # Bundler
23 | .bundle
24 |
25 | Carthage
26 | # We recommend against adding the Pods directory to your .gitignore. However
27 | # you should judge for yourself, the pros and cons are mentioned at:
28 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
29 | #
30 | # Note: if you ignore the Pods directory, make sure to uncomment
31 | # `pod install` in .travis.yml
32 | #
33 | Pods/
34 |
--------------------------------------------------------------------------------
/Example/GlyuckDataGrid/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | }
33 | ],
34 | "info" : {
35 | "version" : 1,
36 | "author" : "xcode"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Example/Tests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # references:
2 | # * http://www.objc.io/issue-6/travis-ci.html
3 | # * https://github.com/supermarin/xcpretty#usage
4 |
5 | language: objective-c
6 | #xcode_workspace: Example/GlyuckDataGrid.xcworkspace
7 | #xcode_scheme: GlyuckDataGrid-Example
8 | cache: cocoapods
9 | podfile: Example/Podfile
10 | osx_image: xcode8
11 | before_install:
12 | - |
13 | BUNDLE_GEMFILE=Example/Gemfile bundle install
14 | BUNDLE_GEMFILE=Example/Gemfile bundle exec pod repo update
15 | BUNDLE_GEMFILE=Example/Gemfile bundle exec pod install --project-directory=Example
16 | install:
17 | - gem install xcpretty --no-rdoc --no-ri --no-document --quiet
18 | script:
19 | - set -o pipefail
20 | - xcodebuild -version
21 | - xcodebuild -showsdks
22 | - xcodebuild test -workspace Example/GlyuckDataGrid.xcworkspace -scheme GlyuckDataGrid-Example -sdk iphonesimulator10.0 -destination 'platform=iOS Simulator,name=iPhone 7,OS=10.0' | xcpretty
23 |
--------------------------------------------------------------------------------
/GlyuckDataGrid.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = "GlyuckDataGrid"
3 | s.version = "0.3.0"
4 | s.summary = "An UICollectionView subclass specialized on displaying multicolumn tables or spreadsheets."
5 | s.homepage = "https://github.com/glyuck/GlyuckDataGrid"
6 | s.screenshots = "https://raw.githubusercontent.com/glyuck/GlyuckDataGrid/master/Example/screenshot_01.png", "https://raw.githubusercontent.com/glyuck/GlyuckDataGrid/master/Example/screenshot_02.png"
7 | s.license = "MIT"
8 | s.author = { "Vladimir Lyukov" => "v.lyukov@gmail.com" }
9 | s.source = { :git => "https://github.com/Glyuck/GlyuckDataGrid.git", :tag => s.version.to_s }
10 |
11 | s.platform = :ios, "8.0"
12 | s.requires_arc = true
13 |
14 | s.source_files = "Source/**/*"
15 |
16 | s.public_header_files = "Source/**/*.h"
17 | s.frameworks = "UIKit"
18 | end
19 |
--------------------------------------------------------------------------------
/Example/GlyuckDataGrid/Examples/SpreadSheet/Views/SpreadSheetCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SpreadSheetCell.swift
3 | // GlyuckDataGrid
4 | //
5 | // Created by Vladimir Lyukov on 16/11/15.
6 | // Copyright © 2015 CocoaPods. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import GlyuckDataGrid
11 |
12 |
13 | protocol SpreadSheetCellDelegate {
14 | func spreadSheetCell(_ cell: SpreadSheetCell, didUpdateData data: String, atIndexPath indexPath: IndexPath)
15 | }
16 |
17 |
18 | class SpreadSheetCell: DataGridViewBaseCell {
19 | @IBOutlet weak var textField: UITextField!
20 | var indexPath: IndexPath!
21 |
22 | var delegate: SpreadSheetCellDelegate?
23 |
24 | func configureWithData(_ data: String, forIndexPath indexPath: IndexPath) {
25 | self.indexPath = indexPath
26 | textField.text = data
27 | }
28 | }
29 |
30 |
31 | extension SpreadSheetCell: UITextFieldDelegate {
32 | func textFieldDidEndEditing(_ textField: UITextField) {
33 | let data = textField.text ?? ""
34 | delegate?.spreadSheetCell(self, didUpdateData: data, atIndexPath: indexPath)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 Vladimir Lyukov
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 |
--------------------------------------------------------------------------------
/Source/Cells/DataGridViewContentCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataGridViewContentCell.swift
3 | //
4 | // Created by Vladimir Lyukov on 03/08/15.
5 | //
6 |
7 | import UIKit
8 |
9 |
10 | /**
11 | Class for default data grid view cell.
12 | */
13 | open class DataGridViewContentCell: DataGridViewBaseCell {
14 | private static var __once: () = {
15 | let appearance = DataGridViewContentCell.appearance()
16 | appearance.textLabelInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
17 |
18 | if let labelAppearance = UILabel.glyuck_appearanceWhenContained(in: DataGridViewContentCell.self) {
19 | if #available(iOS 8.2, *) {
20 | labelAppearance.appearanceFont = UIFont.systemFont(ofSize: 14, weight: UIFontWeightLight)
21 | } else {
22 | labelAppearance.appearanceFont = UIFont(name: "HelveticaNeue-Light", size: 14)
23 | }
24 | labelAppearance.appearanceMinimumScaleFactor = 0.5
25 | labelAppearance.appearanceNumberOfLines = 0
26 | }
27 |
28 | }()
29 | open override static func initialize() {
30 | super.initialize()
31 | _ = DataGridViewContentCell.__once
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Example/GlyuckDataGrid/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UIRequiredDeviceCapabilities
30 |
31 | armv7
32 |
33 | UISupportedInterfaceOrientations
34 |
35 | UIInterfaceOrientationPortrait
36 | UIInterfaceOrientationLandscapeLeft
37 | UIInterfaceOrientationPortraitUpsideDown
38 | UIInterfaceOrientationLandscapeRight
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Example/Tests/Cells/DataGridViewBaseCellSpec.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataGridViewBaseCellSpec.swift
3 | // GlyuckDataGrid
4 | //
5 | // Created by Vladimir Lyukov on 12/08/15.
6 | // Copyright © 2015 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import GlyuckDataGrid
12 |
13 |
14 | class DataGridViewBaseCellSpec: QuickSpec {
15 | override func spec() {
16 | var sut: DataGridViewBaseCell!
17 |
18 | beforeEach {
19 | sut = DataGridViewBaseCell(frame: CGRect(x: 0, y: 0, width: 100, height: 60))
20 | }
21 |
22 | describe("textLabel") {
23 | it("should not be nil") {
24 | expect(sut.textLabel).notTo(beNil())
25 | }
26 |
27 | it("should be subview of contentView") {
28 | expect(sut.textLabel.superview) === sut.contentView
29 | }
30 |
31 | it("should resize along with cell with respect to cell.textLabelInsets") {
32 | sut.textLabel.text = "" // Ensure text label is initialized when tests are started
33 |
34 | sut.textLabelInsets = UIEdgeInsets(top: 1, left: 2, bottom: 3, right: 4)
35 | sut.frame = CGRect(x: 0, y: 0, width: sut.frame.width * 2, height: sut.frame.height / 2)
36 | sut.layoutIfNeeded()
37 | expect(sut.textLabel.frame) == UIEdgeInsetsInsetRect(sut.bounds, sut.textLabelInsets)
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Source/Cells/DataGridViewRowHeaderCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataGridViewRowHeaderCell.swift
3 | // Pods
4 | //
5 | // Created by Vladimir Lyukov on 20/11/15.
6 | //
7 | //
8 |
9 | import UIKit
10 |
11 |
12 | open class DataGridViewRowHeaderCell: DataGridViewBaseHeaderCell {
13 | private static var __once: () = {
14 | let appearance = DataGridViewRowHeaderCell.appearance()
15 | appearance.backgroundColor = UIColor.white
16 | appearance.sortedBackgroundColor = UIColor(white: 220.0/255.0, alpha: 1)
17 | appearance.sortAscSuffix = " →"
18 | appearance.sortDescSuffix = " ←"
19 | appearance.textLabelInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
20 | appearance.borderRightColor = UIColor(white: 0.73, alpha: 1)
21 | appearance.borderRightWidth = 1 / UIScreen.main.scale
22 |
23 | if let labelAppearance = UILabel.glyuck_appearanceWhenContained(in: DataGridViewRowHeaderCell.self) {
24 | if #available(iOS 8.2, *) {
25 | labelAppearance.appearanceFont = UIFont.systemFont(ofSize: 14, weight: UIFontWeightRegular)
26 | } else {
27 | labelAppearance.appearanceFont = UIFont(name: "HelveticaNeue", size: 14)
28 | }
29 | labelAppearance.appearanceAdjustsFontSizeToFitWidth = true
30 | labelAppearance.appearanceMinimumScaleFactor = 0.5
31 | labelAppearance.appearanceNumberOfLines = 0
32 | }
33 |
34 | }()
35 | open override static func initialize() {
36 | super.initialize()
37 | _ = DataGridViewRowHeaderCell.__once
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Example/Tests/TestDoubles/StubDataGridViewDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StubDataGridViewDelegate.swift
3 | //
4 | // Created by Vladimir Lyukov on 03/08/15.
5 | //
6 |
7 | import Foundation
8 | import GlyuckDataGrid
9 |
10 |
11 | class StubDataGridViewDelegate: NSObject, DataGridViewDelegate {
12 | var rowHeight: CGFloat = 70
13 | var columnWidth: CGFloat = 100
14 | var floatingColumns = [Int]()
15 | var shouldSortByColumnBlock: ((Int) -> Bool)?
16 | var didSortByColumnBlock: ((Int) -> Void)?
17 | var shouldSelectRowBlock: ((Int) -> Bool)?
18 | var didSelectRowBlock: ((Int) -> Void)?
19 |
20 | func dataGridView(_ dataGridView: DataGridView, widthForColumn column: Int) -> CGFloat {
21 | return columnWidth
22 | }
23 |
24 | func dataGridView(_ dataGridView: DataGridView, heightForRow row: Int) -> CGFloat {
25 | return rowHeight
26 | }
27 |
28 | func dataGridView(_ dataGridView: DataGridView, shouldFloatColumn column: Int) -> Bool {
29 | return floatingColumns.index(of: column) != nil
30 | }
31 |
32 | func dataGridView(_ dataGridView: DataGridView, shouldSortByColumn column: Int) -> Bool {
33 | return shouldSortByColumnBlock?(column) ?? true
34 | }
35 |
36 | func dataGridView(_ dataGridView: DataGridView, didSortByColumn column: Int) {
37 | didSortByColumnBlock?(column)
38 | }
39 |
40 | func dataGridView(_ dataGridView: DataGridView, shouldSelectRow row: Int) -> Bool {
41 | return shouldSelectRowBlock?(row) ?? true
42 | }
43 |
44 | func dataGridView(_ dataGridView: DataGridView, didSelectRow row: Int) {
45 | didSelectRowBlock?(row)
46 | }
47 | }
48 |
49 |
50 | class StubMinimumDataGridViewDelegate: DataGridViewDelegate {
51 | }
52 |
--------------------------------------------------------------------------------
/Source/Utility/UILabel+AppearanceSelectors.m:
--------------------------------------------------------------------------------
1 | //
2 | // UILabel+AppearanceSelectors.m
3 | // Pods
4 | //
5 | // Created by Vladimir Lyukov on 23/11/15.
6 | //
7 | //
8 |
9 | #import "UILabel+AppearanceSelectors.h"
10 |
11 | @implementation UILabel (AppearanceSelectors)
12 |
13 | - (void)setAppearanceFont:(UIFont *)font {
14 | self.font = font;
15 | }
16 |
17 | - (void)setAppearanceTextColor:(UIColor *)textColor {
18 | self.textColor = textColor;
19 | }
20 |
21 | - (void)setAppearanceShadowColor:(UIColor *)shadowColor {
22 | self.shadowColor = shadowColor;
23 | }
24 |
25 | - (void)setAppearanceShadowOffset:(CGSize)shadowOffset {
26 | self.shadowOffset = shadowOffset;
27 | }
28 |
29 | - (void)setAppearanceTextAlignment:(NSTextAlignment)textAlignment {
30 | self.textAlignment = textAlignment;
31 | }
32 |
33 | - (void)setAppearanceLineBreakMode:(NSLineBreakMode)lineBreakMode {
34 | self.lineBreakMode = lineBreakMode;
35 | }
36 |
37 | - (void)setAppearanceHighlightedTextColor:(UIColor *)highlightedTextColor {
38 | self.highlightedTextColor = highlightedTextColor;
39 | }
40 |
41 | - (void)setAppearanceNumberOfLines:(NSInteger)numberOfLines {
42 | self.numberOfLines = numberOfLines;
43 | }
44 |
45 | - (void)setAppearanceAdjustsFontSizeToFitWidth:(BOOL)adjustsFontSizeToFitWidth {
46 | self.adjustsFontSizeToFitWidth = adjustsFontSizeToFitWidth;
47 | }
48 |
49 | - (void)setAppearanceBaselineAdjustment:(UIBaselineAdjustment)baselineAdjustment {
50 | self.baselineAdjustment = baselineAdjustment;
51 | }
52 |
53 | - (void)setAppearanceMinimumScaleFactor:(CGFloat)minimumScaleFactor {
54 | self.minimumScaleFactor = minimumScaleFactor;
55 | }
56 |
57 | - (void)setAppearanceAllowsDefaultTighteningForTruncation:(BOOL)allowsDefaultTighteningForTruncation {
58 | self.allowsDefaultTighteningForTruncation = allowsDefaultTighteningForTruncation;
59 | }
60 |
61 | @end
62 |
--------------------------------------------------------------------------------
/Example/Tests/Cells/DataGridViewBaseHeaderCellSpec.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataGridViewBaseHeaderCellSpec.swift
3 | // GlyuckDataGrid
4 | //
5 | // Created by Vladimir Lyukov on 24/11/15.
6 | // Copyright © 2015 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import GlyuckDataGrid
12 |
13 |
14 | class DataGridViewBaseHeaderCellSpec: QuickSpec {
15 | override func spec() {
16 | var sut: DataGridViewBaseHeaderCell!
17 |
18 | beforeEach {
19 | sut = DataGridViewBaseHeaderCell(frame: CGRect(x: 0, y: 0, width: 100, height: 60))
20 | }
21 |
22 | describe("DataGridViewBaseHeaderCell") {
23 | it("should have textLabel.title updated according to isSorted and isSortedAsc") {
24 | sut.sortAscSuffix = " ASC"
25 | sut.sortDescSuffix = " DESC"
26 |
27 | sut.title = "Title"
28 | expect(sut.textLabel.text) == "Title"
29 |
30 | sut.isSorted = true
31 | expect(sut.textLabel.text) == "Title ASC"
32 |
33 | sut.isSortedAsc = false
34 | expect(sut.textLabel.text) == "Title DESC"
35 |
36 | sut.title = "Title 2"
37 | expect(sut.textLabel.text) == "Title 2 DESC"
38 |
39 | sut.isSorted = false
40 | expect(sut.textLabel.text) == "Title 2"
41 | }
42 |
43 | it("should update backgroundColor according to isSorted") {
44 | sut.backgroundColor = UIColor.green
45 | sut.sortedBackgroundColor = UIColor.red
46 |
47 | expect(sut.backgroundColor) == UIColor.green
48 |
49 | sut.isSorted = true
50 | expect(sut.backgroundColor) == UIColor.red
51 |
52 | sut.isSorted = false
53 | expect(sut.backgroundColor) == UIColor.green
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Source/Cells/DataGridViewColumnHeaderCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataGridViewColumnHeaderCell.swift
3 | //
4 | // Created by Vladimir Lyukov on 03/08/15.
5 | //
6 |
7 | import UIKit
8 |
9 |
10 | open class DataGridViewColumnHeaderCell: DataGridViewBaseHeaderCell {
11 | private static var __once: () = {
12 | let appearance = DataGridViewColumnHeaderCell.appearance()
13 | appearance.backgroundColor = UIColor.white
14 | appearance.sortedBackgroundColor = UIColor(white: 220.0/255.0, alpha: 1)
15 | appearance.sortAscSuffix = " ↑"
16 | appearance.sortDescSuffix = " ↓"
17 | appearance.textLabelInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
18 | appearance.borderBottomColor = UIColor(white: 0.73, alpha: 1)
19 | appearance.borderBottomWidth = 1 / UIScreen.main.scale
20 |
21 | if let labelAppearance = UILabel.glyuck_appearanceWhenContained(in: DataGridViewColumnHeaderCell.self) {
22 | if #available(iOS 8.2, *) {
23 | labelAppearance.appearanceFont = UIFont.systemFont(ofSize: 14, weight: UIFontWeightRegular)
24 | } else {
25 | labelAppearance.appearanceFont = UIFont(name: "HelveticaNeue", size: 14)
26 | }
27 | labelAppearance.appearanceTextAlignment = .center
28 | labelAppearance.appearanceAdjustsFontSizeToFitWidth = true
29 | labelAppearance.appearanceMinimumScaleFactor = 0.5
30 | labelAppearance.appearanceNumberOfLines = 0
31 | }
32 |
33 | }()
34 | // MARK: - UIView
35 | open override static func initialize() {
36 | super.initialize()
37 | _ = DataGridViewColumnHeaderCell.__once
38 | }
39 |
40 | // MARK: - Custom methods
41 |
42 | open override func didTap(_ gesture: UITapGestureRecognizer) {
43 | dataGridView.collectionViewDelegate.collectionView(dataGridView.collectionView, didTapHeaderForColumn: indexPath.index)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Example/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | activesupport (4.2.7.1)
5 | i18n (~> 0.7)
6 | json (~> 1.7, >= 1.7.7)
7 | minitest (~> 5.1)
8 | thread_safe (~> 0.3, >= 0.3.4)
9 | tzinfo (~> 1.1)
10 | claide (1.0.0)
11 | cocoapods (1.1.0.rc.2)
12 | activesupport (>= 4.0.2, < 5)
13 | claide (>= 1.0.0, < 2.0)
14 | cocoapods-core (= 1.1.0.rc.2)
15 | cocoapods-deintegrate (>= 1.0.1, < 2.0)
16 | cocoapods-downloader (>= 1.1.1, < 2.0)
17 | cocoapods-plugins (>= 1.0.0, < 2.0)
18 | cocoapods-search (>= 1.0.0, < 2.0)
19 | cocoapods-stats (>= 1.0.0, < 2.0)
20 | cocoapods-trunk (>= 1.0.0, < 2.0)
21 | cocoapods-try (>= 1.1.0, < 2.0)
22 | colored (~> 1.2)
23 | escape (~> 0.0.4)
24 | fourflusher (~> 1.0.1)
25 | gh_inspector (~> 1.0)
26 | molinillo (~> 0.5.1)
27 | nap (~> 1.0)
28 | xcodeproj (>= 1.3.1, < 2.0)
29 | cocoapods-core (1.1.0.rc.2)
30 | activesupport (>= 4.0.2, < 5)
31 | fuzzy_match (~> 2.0.4)
32 | nap (~> 1.0)
33 | cocoapods-deintegrate (1.0.1)
34 | cocoapods-downloader (1.1.1)
35 | cocoapods-plugins (1.0.0)
36 | nap
37 | cocoapods-search (1.0.0)
38 | cocoapods-stats (1.0.0)
39 | cocoapods-trunk (1.0.0)
40 | nap (>= 0.8, < 2.0)
41 | netrc (= 0.7.8)
42 | cocoapods-try (1.1.0)
43 | colored (1.2)
44 | escape (0.0.4)
45 | fourflusher (1.0.1)
46 | fuzzy_match (2.0.4)
47 | gh_inspector (1.0.2)
48 | i18n (0.7.0)
49 | json (1.8.3)
50 | minitest (5.9.1)
51 | molinillo (0.5.1)
52 | nap (1.1.0)
53 | netrc (0.7.8)
54 | thread_safe (0.3.5)
55 | tzinfo (1.2.2)
56 | thread_safe (~> 0.1)
57 | xcodeproj (1.3.1)
58 | activesupport (>= 3)
59 | claide (>= 1.0.0, < 2.0)
60 | colored (~> 1.2)
61 |
62 | PLATFORMS
63 | ruby
64 |
65 | DEPENDENCIES
66 | activesupport (~> 4.2.7)
67 | cocoapods (= 1.1.0.rc.2)
68 |
69 | BUNDLED WITH
70 | 1.12.5
71 |
--------------------------------------------------------------------------------
/Source/Utility/IndexPath+DataGrid.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSIndexPath+DataGrid.swift
3 | //
4 | // Created by Vladimir Lyukov on 31/07/15.
5 | //
6 |
7 | import Foundation
8 |
9 |
10 | /**
11 | Custom extension for NSIndexPath to support data grid view rows/columns. You should NOT used indexPath.row to access data grid view row index. Use indexPath.dataGridRow and indexPath.dataGridSection instead.
12 | */
13 | public extension IndexPath {
14 | /**
15 | Returns an index-path object initialized with the indexes of a specific row and column in a data grid view.
16 |
17 | - parameter column: An index number identifying a column in a DataGridView object in a row identified by the row parameter.
18 | - parameter row: An index number identifying a row in a DataGridView object.
19 |
20 | - returns: An NSIndexPath object.
21 | */
22 | init(forColumn column: Int, row: Int) {
23 | self.init(item: column, section: row)
24 | }
25 |
26 | /// An index number identifying a column in a row of a data grid view. (read-only)
27 | var dataGridColumn: Int {
28 | return self[1]
29 | }
30 |
31 | /// An index number identifying a row in a data grid view. (read-only)
32 | var dataGridRow: Int {
33 | return self[0]
34 | }
35 |
36 | /// An index number for single-item indexPath
37 | var index: Int {
38 | return self[0]
39 | }
40 | }
41 |
42 |
43 | public extension NSIndexPath {
44 | /**
45 | Returns an index-path object initialized with the indexes of a specific row and column in a data grid view.
46 |
47 | - parameter column: An index number identifying a column in a DataGridView object in a row identified by the row parameter.
48 | - parameter row: An index number identifying a row in a DataGridView object.
49 |
50 | - returns: An NSIndexPath object.
51 | */
52 | convenience init(forColumn column: Int, row: Int) {
53 | self.init(item: column, section: row)
54 | }
55 |
56 | /// An index number identifying a column in a row of a data grid view. (read-only)
57 | var dataGridColumn: Int {
58 | return self.index(atPosition: 1)
59 | }
60 |
61 | /// An index number identifying a row in a data grid view. (read-only)
62 | var dataGridRow: Int {
63 | return self.index(atPosition: 0)
64 | }
65 |
66 | /// An index number for single-item indexPath
67 | var index: Int {
68 | return self.index(atPosition: 0)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Source/Utility/UILabel+AppearanceSelectors.h:
--------------------------------------------------------------------------------
1 | //
2 | // UILabel+AppearanceSelectors.h
3 | // Pods
4 | //
5 | // Created by Vladimir Lyukov on 23/11/15.
6 | //
7 | //
8 |
9 | #import
10 |
11 | @interface UILabel (AppearanceSelectors)
12 |
13 | @property(null_resettable, nonatomic, strong) UIFont *appearanceFont UI_APPEARANCE_SELECTOR;
14 | @property(null_resettable, nonatomic, strong) UIColor *appearanceTextColor UI_APPEARANCE_SELECTOR;
15 | @property(nullable, nonatomic,strong) UIColor *appearanceShadowColor UI_APPEARANCE_SELECTOR;
16 | @property(nonatomic) CGSize appearanceShadowOffset UI_APPEARANCE_SELECTOR;
17 | @property(nonatomic) NSTextAlignment appearanceTextAlignment UI_APPEARANCE_SELECTOR;
18 | @property(nonatomic) NSLineBreakMode appearanceLineBreakMode UI_APPEARANCE_SELECTOR;
19 | @property(nullable, nonatomic,strong) UIColor *appearanceHighlightedTextColor UI_APPEARANCE_SELECTOR;
20 | @property(nonatomic) NSInteger appearanceNumberOfLines UI_APPEARANCE_SELECTOR;
21 | @property(nonatomic) BOOL appearanceAdjustsFontSizeToFitWidth UI_APPEARANCE_SELECTOR;
22 | @property(nonatomic) UIBaselineAdjustment appearanceBaselineAdjustment UI_APPEARANCE_SELECTOR;
23 | @property(nonatomic) CGFloat appearanceMinimumScaleFactor NS_AVAILABLE_IOS(6_0) UI_APPEARANCE_SELECTOR;
24 | @property(nonatomic) BOOL appearanceAllowsDefaultTighteningForTruncation NS_AVAILABLE_IOS(9_0) UI_APPEARANCE_SELECTOR;
25 |
26 | - (nullable UIFont *)appearanceFont UNAVAILABLE_ATTRIBUTE;
27 | - (nullable UIColor *)appearanceTextColor UNAVAILABLE_ATTRIBUTE;
28 | - (nullable UIColor *)appearanceShadowColor UNAVAILABLE_ATTRIBUTE;
29 | - (CGSize)appearanceShadowOffset UNAVAILABLE_ATTRIBUTE;
30 | - (NSTextAlignment)appearanceTextAlignment UNAVAILABLE_ATTRIBUTE;
31 | - (NSLineBreakMode)appearanceLineBreakMode UNAVAILABLE_ATTRIBUTE;
32 | - (nullable UIColor *)appearanceHighlightedTextColor UNAVAILABLE_ATTRIBUTE;
33 | - (NSInteger)appearanceNumberOfLines UNAVAILABLE_ATTRIBUTE;
34 | - (BOOL)appearanceAdjustsFontSizeToFitWidth UNAVAILABLE_ATTRIBUTE;
35 | - (UIBaselineAdjustment)appearanceBaselineAdjustment UNAVAILABLE_ATTRIBUTE;
36 | - (CGFloat)appearanceMinimumScaleFactor UNAVAILABLE_ATTRIBUTE;
37 | - (BOOL)appearanceAllowsDefaultTighteningForTruncation UNAVAILABLE_ATTRIBUTE;
38 |
39 | @end
40 |
--------------------------------------------------------------------------------
/Source/CollectionViewDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CollectionViewDelegate.swift
3 | // Pods
4 | //
5 | // Created by Vladimir Lyukov on 31/07/15.
6 | //
7 | //
8 |
9 | import UIKit
10 |
11 |
12 | /**
13 | This class incapsulates logic for delegate of UICollectionView used internally by DataGridView. You should not use this class directly.
14 | */
15 | open class CollectionViewDelegate: NSObject, UICollectionViewDelegate {
16 | fileprivate(set) open weak var dataGridView: DataGridView!
17 |
18 | init(dataGridView: DataGridView) {
19 | self.dataGridView = dataGridView
20 | super.init()
21 | }
22 |
23 | open func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
24 | return dataGridView.delegate?.dataGridView?(dataGridView, shouldSelectRow: (indexPath as NSIndexPath).section) ?? true
25 | }
26 |
27 | open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
28 | dataGridView.selectRow((indexPath as NSIndexPath).section, animated: false)
29 | dataGridView.delegate?.dataGridView?(dataGridView, didSelectRow: (indexPath as NSIndexPath).section)
30 | }
31 |
32 | open func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
33 | dataGridView.deselectRow((indexPath as NSIndexPath).section , animated: false)
34 | }
35 |
36 | open func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath) {
37 | dataGridView.highlightRow((indexPath as NSIndexPath).section)
38 | }
39 |
40 | open func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath) {
41 | dataGridView.unhighlightRow((indexPath as NSIndexPath).section)
42 | }
43 |
44 | // MARK: - Custom delegate methods
45 |
46 | open func collectionView(_ collectionView: UICollectionView, shouldHighlightHeaderForColumn column: Int) -> Bool {
47 | return dataGridView.delegate?.dataGridView?(dataGridView, shouldSortByColumn: column) ?? false
48 | }
49 |
50 | open func collectionView(_ collectionView: UICollectionView, didTapHeaderForColumn column: Int) {
51 | if dataGridView.delegate?.dataGridView?(dataGridView, shouldSortByColumn: column) == true {
52 | if dataGridView.sortColumn == column {
53 | dataGridView.setSortColumn(column, ascending: !dataGridView.sortAscending)
54 | } else {
55 | dataGridView.setSortColumn(column, ascending: true)
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Example/Tests/Utility/BorderHelperSpec.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BorderHelperSpec.swift
3 | //
4 | // Created by Vladimir Lyukov on 13/08/15.
5 | //
6 |
7 | import Quick
8 | import Nimble
9 | import GlyuckDataGrid
10 |
11 |
12 | class BorderHelperSpec: QuickSpec {
13 | override func spec() {
14 | var view: UIView!
15 | var sut: BorderHelper!
16 |
17 | beforeEach {
18 | view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
19 | sut = BorderHelper(view: view)
20 | }
21 |
22 | describe("BorderHelper") {
23 | describe(".topBorder") {
24 | describe("Layer") {
25 | it("should be nil if width == 0") {
26 | sut.topWidth = 0
27 | expect(sut.topLayer).to(beNil())
28 | }
29 | it("should not be nil if width != 0") {
30 | sut.topWidth = 1
31 | expect(sut.topLayer).notTo(beNil())
32 | }
33 | it("should be sublayer of view.layer") {
34 | sut.topWidth = 1
35 | expect(sut.topLayer?.superlayer) === view.layer
36 | }
37 | it("should have correct background color after it's created") {
38 | sut.topColor = UIColor.red // Assign border color before border is created
39 | sut.topWidth = 1
40 |
41 | let isEqual = sut.topLayer!.backgroundColor == UIColor.red.cgColor
42 | expect(isEqual).to(beTrue())
43 | }
44 | it("should be removed from superlayer and deallocated if width became 0") {
45 | sut.topWidth = 1
46 | expect(sut.topLayer).notTo(beNil())
47 | expect(view.layer.sublayers?.count) == 1
48 |
49 | sut.topWidth = 0
50 | expect(sut.topLayer).to(beNil())
51 | expect(view.layer.sublayers).to(beNil())
52 | }
53 | }
54 | describe("Color") {
55 | it("should be assigned to corresponding layer.backgroundColor") {
56 | sut.topWidth = 1 // Ensure layer already created
57 |
58 | sut.topColor = UIColor.green
59 |
60 | let isEqual = sut.topColor.cgColor == sut?.topLayer?.backgroundColor
61 | expect(isEqual).to(beTrue())
62 | }
63 | }
64 | }
65 | }
66 | // Will just hope that all other borders are working same way as topBorder
67 | // Don't want to copy-paste this tests 3 times
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Example/Tests/TestDoubles/StubDataGridViewDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StubDataGridViewDataSource.swift
3 | //
4 | // Created by Vladimir Lyukov on 03/08/15.
5 | //
6 |
7 | import UIKit
8 | import GlyuckDataGrid
9 |
10 |
11 | class StubDataGridViewDataSource: NSObject, DataGridViewDataSource {
12 | var numberOfColumns = 7
13 | var numberOfRows = 20
14 |
15 | func numberOfColumnsInDataGridView(_ dataGridView: DataGridView) -> Int {
16 | return numberOfColumns
17 | }
18 |
19 | func numberOfRowsInDataGridView(_ dataGridView: DataGridView) -> Int {
20 | return numberOfRows
21 | }
22 |
23 | func dataGridView(_ dataGridView: DataGridView, titleForHeaderForColumn column: Int) -> String {
24 | return "Title for column \(column)"
25 | }
26 |
27 | func dataGridView(_ dataGridView: DataGridView, titleForHeaderForRow row: Int) -> String {
28 | return "Title for row \(row)"
29 | }
30 |
31 | func dataGridView(_ dataGridView: DataGridView, textForCellAtIndexPath indexPath: IndexPath) -> String {
32 | return "Text for cell \(indexPath.dataGridColumn)x\(indexPath.dataGridRow)"
33 | }
34 | }
35 |
36 |
37 | class StubDataGridViewDataSourceCustomCell: StubDataGridViewDataSource {
38 | var cellForItemBlock: ((dataGridView: DataGridView, indexPath: IndexPath)) -> UICollectionViewCell = { (dataGridView, indexPath) in
39 | let cell = dataGridView.dequeueReusableCellWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultCell, forIndexPath: indexPath)
40 | cell.tag = indexPath.dataGridColumn * 100 + indexPath.dataGridRow
41 | return cell
42 | }
43 |
44 | var viewForColumnHeaderBlock: ((dataGridView: DataGridView, column: Int)) -> DataGridViewColumnHeaderCell = { (dataGridView, column) in
45 | let view = dataGridView.dequeueReusableHeaderViewWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultColumnHeader, forColumn: column)
46 | view.tag = column
47 | return view
48 | }
49 |
50 | var viewForRowHeaderBlock: ((dataGridView: DataGridView, row: Int)) -> DataGridViewRowHeaderCell = { (dataGridView, row) in
51 | let view = dataGridView.dequeueReusableHeaderViewWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultRowHeader, forRow: row)
52 | view.tag = row
53 | return view
54 | }
55 |
56 | func dataGridView(_ dataGridView: DataGridView, viewForHeaderForColumn column: Int) -> DataGridViewColumnHeaderCell {
57 | return viewForColumnHeaderBlock((dataGridView: dataGridView, column: column))
58 | }
59 |
60 | func dataGridView(_ dataGridView: DataGridView, viewForHeaderForRow row: Int) -> DataGridViewRowHeaderCell {
61 | return viewForRowHeaderBlock((dataGridView: dataGridView, row: row))
62 | }
63 |
64 | func dataGridView(_ dataGridView: DataGridView, cellForItemAtIndexPath indexPath: IndexPath) -> UICollectionViewCell {
65 | return cellForItemBlock((dataGridView: dataGridView, indexPath: indexPath))
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Example/GlyuckDataGrid/Examples/SpreadSheet/Views/SpreadSheetCell.xib:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/Source/Cells/DataGridViewBaseCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataGridViewBaseCell.swift
3 | // Pods
4 | //
5 | // Created by Vladimir Lyukov on 12/08/15.
6 | //
7 | //
8 |
9 | import UIKit
10 |
11 |
12 | /**
13 | Base class for data grid view cells.
14 | */
15 | open class DataGridViewBaseCell: UICollectionViewCell {
16 | /// The inset or outset margins for the rectangle around the cell’s text label.
17 | open dynamic var textLabelInsets = UIEdgeInsets.zero
18 | /// Background color for highlighted state.
19 | open dynamic var highlightedBackgroundColor = UIColor(white: 0.9, alpha: 1)
20 | /// Background color for selected state.
21 | open dynamic var selectedBackgroundColor = UIColor(white: 0.8, alpha: 1)
22 | /// Helper object for configuring cell borders.
23 | open lazy var border: BorderHelper = {
24 | BorderHelper(view: self)
25 | }()
26 |
27 | /// Returns the label used for the main textual content of the table cell. (read-only)
28 | fileprivate(set) open lazy var textLabel: UILabel = {
29 | let label = UILabel(frame: self.bounds)
30 | self.contentView.addSubview(label)
31 | return label
32 | }()
33 |
34 | // MARK: - UICollectionViewCell
35 |
36 | open override var isHighlighted: Bool {
37 | didSet {
38 | contentView.backgroundColor = isHighlighted ? highlightedBackgroundColor : UIColor.clear
39 | }
40 | }
41 |
42 | open override var isSelected: Bool {
43 | didSet {
44 | contentView.backgroundColor = isSelected ? selectedBackgroundColor : UIColor.clear
45 | }
46 | }
47 |
48 | open override func layoutSubviews() {
49 | super.layoutSubviews()
50 | textLabel.frame = UIEdgeInsetsInsetRect(bounds, textLabelInsets)
51 | }
52 |
53 | open override func layoutSublayers(of layer: CALayer) {
54 | super.layoutSublayers(of: layer)
55 | if layer == self.layer {
56 | border.layoutLayersInFrame(layer.frame)
57 | }
58 | }
59 | }
60 |
61 | // Border getters/setters for UIAppearance
62 | extension DataGridViewBaseCell {
63 | public dynamic var borderTopWidth: CGFloat {
64 | get { return border.topWidth }
65 | set { border.topWidth = newValue }
66 | }
67 | public dynamic var borderTopColor: UIColor {
68 | get { return border.topColor }
69 | set { border.topColor = newValue }
70 | }
71 | public dynamic var borderLeftWidth: CGFloat {
72 | get { return border.leftWidth }
73 | set { border.leftWidth = newValue }
74 | }
75 | public dynamic var borderLeftColor: UIColor {
76 | get { return border.leftColor }
77 | set { border.leftColor = newValue }
78 | }
79 | public dynamic var borderBottomWidth: CGFloat {
80 | get { return border.bottomWidth }
81 | set { border.bottomWidth = newValue }
82 | }
83 | public dynamic var borderBottomColor: UIColor {
84 | get { return border.bottomColor }
85 | set { border.bottomColor = newValue }
86 | }
87 | public dynamic var borderRightWidth: CGFloat {
88 | get { return border.rightWidth }
89 | set { border.rightWidth = newValue }
90 | }
91 |
92 | public dynamic var borderRightColor: UIColor {
93 | get { return border.rightColor }
94 | set { border.rightColor = newValue }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/Source/Cells/DataGridViewBaseHeaderCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataGridViewBaseHeaderCell.swift
3 | // Pods
4 | //
5 | // Created by Vladimir Lyukov on 20/11/15.
6 | //
7 | //
8 |
9 | //
10 | // DataGridViewColumnHeaderCell.swift
11 | //
12 | // Created by Vladimir Lyukov on 03/08/15.
13 | //
14 |
15 | import UIKit
16 |
17 |
18 | /// Base class for sortable and tapable headers
19 | open class DataGridViewBaseHeaderCell: DataGridViewBaseCell {
20 | fileprivate var normalBackgroundColor: UIColor? {
21 | didSet {
22 | updateSortedTitleAndBackground()
23 | }
24 | }
25 | /// Background color for sorted state
26 | open dynamic var sortedBackgroundColor: UIColor? {
27 | didSet {
28 | updateSortedTitleAndBackground()
29 | }
30 | }
31 | open override dynamic var backgroundColor: UIColor? {
32 | get {
33 | return super.backgroundColor
34 | }
35 | set {
36 | normalBackgroundColor = newValue
37 | }
38 | }
39 | /// This suffix will be appended to title if column/row is sorted in ascending order.
40 | open dynamic var sortAscSuffix: String?
41 | /// This suffix will be appended to title if column/row is sorted in descending order.
42 | open dynamic var sortDescSuffix: String?
43 | /// Header title. Use this property instead of assigning to textLabel.text.
44 | open var title: String = "" {
45 | didSet {
46 | updateSortedTitleAndBackground()
47 | }
48 | }
49 | /// Is this header in sorted state (i.e. has sortedBackgroundColor and sortAscSuffix/sortDescSuffix applied)
50 | open var isSorted: Bool = false {
51 | didSet {
52 | updateSortedTitleAndBackground()
53 | }
54 | }
55 | /// Is this header in sorted ascending or descending order? Only taken into account if isSorted == true.
56 | open var isSortedAsc: Bool = true {
57 | didSet {
58 | updateSortedTitleAndBackground()
59 | }
60 | }
61 | open var dataGridView: DataGridView!
62 | open var indexPath: IndexPath!
63 |
64 | // MARK: - UIView
65 |
66 | public override init(frame: CGRect) {
67 | super.init(frame: frame)
68 | setupDataGridViewHeaderCell()
69 | }
70 |
71 | public required init?(coder aDecoder: NSCoder) {
72 | super.init(coder: aDecoder)
73 | setupDataGridViewHeaderCell()
74 | }
75 |
76 | // MARK: - Custom methods
77 |
78 | open func updateSortedTitleAndBackground() {
79 | if isSorted {
80 | textLabel.text = title + ((isSortedAsc ? sortAscSuffix : sortDescSuffix) ?? "")
81 | super.backgroundColor = sortedBackgroundColor
82 | } else {
83 | textLabel.text = title
84 | super.backgroundColor = normalBackgroundColor
85 | }
86 | }
87 |
88 | open func setupDataGridViewHeaderCell() {
89 | let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(DataGridViewBaseHeaderCell.didTap(_:)))
90 | contentView.addGestureRecognizer(tapGestureRecognizer)
91 | }
92 |
93 | open func configureForDataGridView(_ dataGridView: DataGridView, indexPath: IndexPath) {
94 | self.dataGridView = dataGridView
95 | self.indexPath = indexPath
96 | }
97 |
98 | open func didTap(_ gesture: UITapGestureRecognizer) {
99 | dataGridView.collectionViewDelegate.collectionView(dataGridView.collectionView, didTapHeaderForColumn: indexPath.index)
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/Example/GlyuckDataGrid/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
20 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GlyuckDataGrid
2 | [](https://travis-ci.org/Glyuck/GlyuckDataGrid)
3 | [](http://cocoapods.org/pods/GlyuckDataGrid)
4 | [](https://cocoapods.org/pods/GlyuckDataGrid/quality)
5 | [](http://cocoapods.org/pods/GlyuckDataGrid)
6 | [](http://cocoapods.org/pods/GlyuckDataGrid)
7 |
8 | The `GlyuckDataGrid` is a custom view intended to render multicolumn tables (aka data grids, spreadsheets). Uses `UICollectionView` with custom `UICollectionViewLayout` internally.
9 |
10 |  
11 |
12 | ## Usage
13 |
14 | ### Minimum working example
15 |
16 | ```swift
17 | import UIKit
18 | import GlyuckDataGrid
19 |
20 |
21 | class MultiplicationTableViewController: UIViewController, DataGridViewDataSource {
22 | // You can create view outlet in a Storyboard
23 | @IBOutlet weak var dataGridView: DataGridView!
24 |
25 | override func viewDidLoad() {
26 | super.viewDidLoad()
27 |
28 | //// You can also create view manually
29 | // dataGridView = DataGridView(frame: view.bounds)
30 | // view.addSubview(dataGridView)
31 | //// You'll need to setup constraints for just created view
32 | // dataGridView.setTranslatesAutoresizingMaskIntoConstraints(false)
33 | // view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0.0-[dataGridView]-0.0-|", options: nil, metrics: nil, views: ["dataGridView": dataGridView]))
34 | // view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0.0-[dataGridView]-0.0-|", options: nil, metrics: nil, views: ["dataGridView": dataGridView]))
35 |
36 | // Don't forget to set dataSource and (optionally) delegate
37 | dataGridView.dataSource = self
38 | // dataGridView.delegate = self
39 | }
40 |
41 | // MARK: - DataGridViewDataSource
42 |
43 | // You'll need to tell number of columns in data grid view
44 | func numberOfColumnsInDataGridView(dataGridView: DataGridView) -> Int {
45 | return 9
46 | }
47 |
48 | // And number of rows
49 | func numberOfRowsInDataGridView(dataGridView: DataGridView) -> Int {
50 | return 9
51 | }
52 |
53 | // Then you'll need to provide titles for columns headers
54 | func dataGridView(dataGridView: DataGridView, titleForHeaderForRow row: Int) -> String {
55 | return String(row + 1)
56 | }
57 |
58 | // And rows headers
59 | func dataGridView(dataGridView: DataGridView, titleForHeaderForColumn column: Int) -> String {
60 | return String(column + 1)
61 | }
62 |
63 | // And for text for content cells
64 | func dataGridView(dataGridView: DataGridView, textForCellAtIndexPath indexPath: NSIndexPath) -> String {
65 | return String( (indexPath.dataGridRow + 1) * (indexPath.dataGridColumn + 1) )
66 | }
67 | }
68 | ```
69 |
70 | ### CocoaPods
71 |
72 | To run the example project, run `pod try`. If you manually clone the repo, and run `pod install` from the Example directory first.
73 |
74 | ## Installation
75 |
76 | GlyuckDataGrid is available through [CocoaPods](http://cocoapods.org). To install
77 | it, simply add the following line to your Podfile:
78 |
79 | ```ruby
80 | pod "GlyuckDataGrid"
81 | ```
82 |
83 | ## Author
84 |
85 | Vladimir Lyukov, v.lyukov@gmail.com
86 |
87 | [glyuck.com](http://glyuck.com/)
88 |
89 | ## License
90 |
91 | GlyuckDataGrid is available under the MIT license. See the LICENSE file for more info.
92 |
--------------------------------------------------------------------------------
/Example/GlyuckDataGrid/Examples/SimpleDataGrid/SimpleDataGridViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SimpleDataGridViewController.swift
3 | //
4 | // Created by Vladimir Lyukov on 07/30/2015.
5 | //
6 |
7 | import UIKit
8 | import GlyuckDataGrid
9 | fileprivate func < (lhs: T?, rhs: T?) -> Bool {
10 | switch (lhs, rhs) {
11 | case let (l?, r?):
12 | return l < r
13 | case (nil, _?):
14 | return true
15 | default:
16 | return false
17 | }
18 | }
19 |
20 |
21 |
22 | class SimpleDataGridViewController: UIViewController, DataGridViewDataSource, DataGridViewDelegate {
23 | var dataSource = F1DataSource.stats
24 |
25 | @IBOutlet weak var dataGridView: DataGridView!
26 | override func viewDidLoad() {
27 | super.viewDidLoad()
28 |
29 | dataGridView.dataSource = self
30 | dataGridView.delegate = self
31 | }
32 |
33 | // MARK: DataGridViewDataSource
34 |
35 | func numberOfColumnsInDataGridView(_ dataGridView: DataGridView) -> Int {
36 | return F1DataSource.columns.count
37 | }
38 |
39 | func numberOfRowsInDataGridView(_ dataGridView: DataGridView) -> Int {
40 | return dataSource.count
41 | }
42 |
43 | func dataGridView(_ dataGridView: DataGridView, titleForHeaderForColumn column: Int) -> String {
44 | return F1DataSource.columnsTitles[column]
45 | }
46 |
47 | func dataGridView(_ dataGridView: DataGridView, textForCellAtIndexPath indexPath: IndexPath) -> String {
48 | let fieldName = F1DataSource.columns[indexPath.dataGridColumn]
49 | return dataSource[indexPath.dataGridRow][fieldName]!
50 | }
51 |
52 | func dataGridView(_ dataGridView: DataGridView, viewForHeaderForColumn column: Int) -> DataGridViewColumnHeaderCell {
53 | let cell = dataGridView.dequeueReusableHeaderViewWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultColumnHeader, forColumn: column)
54 | cell.title = self.dataGridView(dataGridView, titleForHeaderForColumn: column)
55 | if column == 1 {
56 | cell.border.rightWidth = 1 / UIScreen.main.scale
57 | cell.border.rightColor = UIColor(white: 0.72, alpha: 1)
58 | } else {
59 | cell.border.rightWidth = 0
60 | }
61 | return cell
62 | }
63 |
64 | func dataGridView(_ dataGridView: DataGridView, cellForItemAtIndexPath indexPath: IndexPath) -> UICollectionViewCell {
65 | let cell = dataGridView.dequeueReusableCellWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultCell, forIndexPath: indexPath) as! DataGridViewContentCell
66 | cell.textLabel.text = self.dataGridView(dataGridView, textForCellAtIndexPath: indexPath)
67 | switch indexPath.dataGridColumn {
68 | case 0,2,5,6,7,8,9,11:
69 | cell.textLabel.textAlignment = .right
70 | default:
71 | cell.textLabel.textAlignment = .left
72 | }
73 | if indexPath.dataGridColumn == 1 {
74 | cell.border.rightWidth = 1 / UIScreen.main.scale
75 | cell.border.rightColor = UIColor(white: 0.72, alpha: 1)
76 | } else {
77 | cell.border.rightWidth = 0
78 | }
79 | return cell
80 | }
81 |
82 | // MARK: DataGridViewDelegate
83 |
84 | func dataGridView(_ dataGridView: DataGridView, widthForColumn column: Int) -> CGFloat {
85 | return F1DataSource.columnsWidths[column]
86 | }
87 |
88 | func dataGridView(_ dataGridView: DataGridView, shouldFloatColumn column: Int) -> Bool {
89 | return column == 1
90 | }
91 |
92 | func dataGridView(_ dataGridView: DataGridView, shouldSortByColumn column: Int) -> Bool {
93 | return true
94 | }
95 |
96 | func dataGridView(_ dataGridView: DataGridView, didSortByColumn column: Int, ascending: Bool) {
97 | let columnName = F1DataSource.columns[column]
98 | dataSource = F1DataSource.stats.sorted { ($0[columnName] < $1[columnName]) == ascending }
99 | dataGridView.reloadData()
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/Example/GlyuckDataGrid/Examples/MultiplicationTable/MultiplicationTableViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MultiplicationTableViewController.swift
3 | // GlyuckDataGrid
4 | //
5 | // Created by Vladimir Lyukov on 19/11/15.
6 | // Copyright © 2015 CocoaPods. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import GlyuckDataGrid
11 |
12 |
13 | class MultiplicationTableViewController: UIViewController, DataGridViewDataSource, DataGridViewDelegate {
14 | @IBOutlet weak var dataGridView: DataGridView!
15 |
16 | static override func initialize() {
17 | super.initialize()
18 |
19 | let dataGridAppearance = DataGridView.glyuck_appearanceWhenContained(in: self)!
20 | dataGridAppearance.row1BackgroundColor = nil
21 | dataGridAppearance.row2BackgroundColor = nil
22 |
23 | let cornerHeaderAppearance = DataGridViewCornerHeaderCell.glyuck_appearanceWhenContained(in: self)!
24 | cornerHeaderAppearance.backgroundColor = UIColor.white
25 | cornerHeaderAppearance.borderBottomWidth = 1 / UIScreen.main.scale
26 | cornerHeaderAppearance.borderBottomColor = UIColor(white: 0.73, alpha: 1)
27 | cornerHeaderAppearance.borderRightWidth = 1 / UIScreen.main.scale
28 | cornerHeaderAppearance.borderRightColor = UIColor(white: 0.73, alpha: 1)
29 |
30 | let rowHeaderAppearance = DataGridViewRowHeaderCell.glyuck_appearanceWhenContained(in: self)!
31 | rowHeaderAppearance.backgroundColor = UIColor(white: 0.95, alpha: 1)
32 | rowHeaderAppearance.borderBottomWidth = 1 / UIScreen.main.scale
33 | rowHeaderAppearance.borderBottomColor = UIColor(white: 0.73, alpha: 1)
34 |
35 | let columnHeaderAppearance = DataGridViewColumnHeaderCell.glyuck_appearanceWhenContained(in: self)!
36 | columnHeaderAppearance.borderRightWidth = 1 / UIScreen.main.scale
37 | columnHeaderAppearance.borderRightColor = UIColor(white: 0.73, alpha: 1)
38 |
39 | let cellAppearance = DataGridViewContentCell.glyuck_appearanceWhenContained(in: self)!
40 | cellAppearance.borderRightWidth = 1 / UIScreen.main.scale
41 | cellAppearance.borderRightColor = UIColor(white: 0.73, alpha: 1)
42 | cellAppearance.borderBottomWidth = 1 / UIScreen.main.scale
43 | cellAppearance.borderBottomColor = UIColor(white: 0.73, alpha: 1)
44 |
45 | columnHeaderAppearance.backgroundColor = UIColor(white: 0.95, alpha: 1)
46 | let labelAppearance = UILabel.glyuck_appearanceWhenContained(in: self)!
47 | labelAppearance.appearanceFont = UIFont.systemFont(ofSize: 12, weight: UIFontWeightLight)
48 | labelAppearance.appearanceTextAlignment = .center
49 | }
50 |
51 | override func viewDidLoad() {
52 | dataGridView.delegate = self
53 | dataGridView.dataSource = self
54 |
55 | dataGridView.rowHeaderWidth = 30
56 | dataGridView.columnHeaderHeight = 30
57 | }
58 |
59 | // MARK: - DataGridViewDataSource
60 |
61 | func numberOfRowsInDataGridView(_ dataGridView: DataGridView) -> Int {
62 | return 9
63 | }
64 |
65 | func numberOfColumnsInDataGridView(_ dataGridView: DataGridView) -> Int {
66 | return 9
67 | }
68 |
69 | func dataGridView(_ dataGridView: DataGridView, titleForHeaderForRow row: Int) -> String {
70 | return String(row + 1)
71 | }
72 |
73 | func dataGridView(_ dataGridView: DataGridView, titleForHeaderForColumn column: Int) -> String {
74 | return String(column + 1)
75 | }
76 |
77 | func dataGridView(_ dataGridView: DataGridView, textForCellAtIndexPath indexPath: IndexPath) -> String {
78 | return String( (indexPath.dataGridRow + 1) * (indexPath.dataGridColumn + 1) )
79 | }
80 |
81 | // MARK: - DataGridViewDelegate
82 |
83 | func dataGridView(_ dataGridView: DataGridView, shouldSelectRow row: Int) -> Bool {
84 | return false
85 | }
86 |
87 | func dataGridView(_ dataGridView: DataGridView, heightForRow row: Int) -> CGFloat {
88 | if let layout = dataGridView.collectionView.collectionViewLayout as? DataGridViewLayout {
89 | return layout.widthForColumn(row)
90 | }
91 | return 44
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Source/Utility/BorderHelper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BorderHelper.swift
3 | // Pods
4 | //
5 | // Created by Vladimir Lyukov on 13/08/15.
6 | //
7 | //
8 |
9 | import UIKit
10 |
11 |
12 | /**
13 | Helper class for adding and configuring borders to UIView. Can configure top/left/bottom/right borders separately.
14 | */
15 | open class BorderHelper {
16 | /// Top border width (in points).
17 | open var topWidth = CGFloat(0) {
18 | didSet { updateLayer(&topLayer, forBorderWidth: topWidth, color: topColor) }
19 | }
20 | /// Top border color.
21 | open var topColor: UIColor = UIColor.black {
22 | didSet { topLayer?.backgroundColor = topColor.cgColor }
23 | }
24 | /// Left border width (in points).
25 | open var leftWidth = CGFloat(0) {
26 | didSet { updateLayer(&leftLayer, forBorderWidth: leftWidth, color: leftColor) }
27 | }
28 | /// Left border color.
29 | open var leftColor: UIColor = UIColor.black {
30 | didSet { leftLayer?.backgroundColor = leftColor.cgColor }
31 | }
32 | /// Bottom border width (in points).
33 | open var bottomWidth = CGFloat(0) {
34 | didSet { updateLayer(&bottomLayer, forBorderWidth: bottomWidth, color: bottomColor) }
35 | }
36 | /// Bottom border color.
37 | open var bottomColor: UIColor = UIColor.black {
38 | didSet { bottomLayer?.backgroundColor = bottomColor.cgColor }
39 | }
40 | /// Right border width (in points.
41 | open var rightWidth = CGFloat(0) {
42 | didSet { updateLayer(&rightLayer, forBorderWidth: rightWidth, color: rightColor) }
43 | }
44 | /// Right border color.
45 | open var rightColor: UIColor = UIColor.black {
46 | didSet { rightLayer?.backgroundColor = rightColor.cgColor }
47 | }
48 |
49 | /// Layer used to render top border.
50 | open var topLayer: CALayer?
51 | /// Layer used to render left border.
52 | open var leftLayer: CALayer?
53 | /// Layer user to render bottom border.
54 | open var bottomLayer: CALayer?
55 | /// Layer used to render right border.
56 | open var rightLayer: CALayer?
57 |
58 | /// Main view to add borders to.
59 | fileprivate weak var view: UIView!
60 |
61 | /**
62 | Creates and returns border helper for the specified view.
63 |
64 | - parameter view: UIView to add borders to.
65 |
66 | - returns: A newly created border helper.
67 | */
68 | public init(view: UIView) {
69 | self.view = view
70 | }
71 |
72 | /**
73 | Creates/destroys layer for border with specified width and color. If width is zero, layer is removed from superview and deallocated.
74 |
75 | - parameter layer: Pointer to border layer to be created/destoyed.
76 | - parameter width: border width (in points).
77 | - parameter color: border color.
78 | */
79 | fileprivate func updateLayer(_ layer:inout CALayer?, forBorderWidth width: CGFloat, color: UIColor) {
80 | if width == 0 {
81 | layer?.removeFromSuperlayer()
82 | layer = nil
83 | } else if layer == nil {
84 | layer = CALayer()
85 | layer!.backgroundColor = color.cgColor
86 | view.layer.addSublayer(layer!)
87 | }
88 | view.layer.setNeedsLayout()
89 | }
90 |
91 | /**
92 | Updates borders positions and sizes according to view frame. You should call this function from view.layoutSublayersOfLayer.
93 |
94 | - parameter frame: Parent view frame rectangle.
95 | */
96 | open func layoutLayersInFrame(_ frame: CGRect) {
97 | topLayer?.backgroundColor = topColor.cgColor
98 | topLayer?.frame = CGRect(x: 0, y: 0, width: frame.width, height: topWidth)
99 |
100 | leftLayer?.backgroundColor = leftColor.cgColor
101 | leftLayer?.frame = CGRect(x: 0, y: 0, width: leftWidth, height: frame.height)
102 |
103 | bottomLayer?.backgroundColor = bottomColor.cgColor
104 | bottomLayer?.frame = CGRect(x: 0, y: frame.height - bottomWidth, width: frame.width, height: bottomWidth)
105 |
106 | rightLayer?.backgroundColor = rightColor.cgColor
107 | rightLayer?.frame = CGRect(x: frame.width - rightWidth, y: 0, width: rightWidth, height: frame.height)
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/Example/GlyuckDataGrid/Examples/ObjectiveC/ObjectiveCDataGridViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // ObjectiveCDataGridViewController.m
3 | // GlyuckDataGrid
4 | //
5 | // Created by Vladimir Lyukov on 24/11/2016.
6 | // Copyright © 2016 CocoaPods. All rights reserved.
7 | //
8 |
9 | #import "ObjectiveCDataGridViewController.h"
10 | #import "GlyuckDataGrid-Swift.h"
11 | #import "GlyuckDataGrid_Example-Swift.h"
12 |
13 |
14 | @interface ObjectiveCDataGridViewController ()
15 |
16 | @property (copy, nonatomic) NSArray *dataSource;
17 |
18 | @property (weak, nonatomic) IBOutlet DataGridView *dataGridView;
19 |
20 | @end
21 |
22 |
23 | @implementation ObjectiveCDataGridViewController
24 |
25 | - (void)viewDidLoad {
26 | [super viewDidLoad];
27 |
28 | self.dataSource = F1DataSource.stats;
29 |
30 | self.dataGridView.dataSource = self;
31 | self.dataGridView.delegate = self;
32 | }
33 |
34 | #pragma mark - DataGridViewDataSource
35 |
36 | - (NSInteger)numberOfColumnsInDataGridView:(DataGridView *)dataGridView {
37 | return F1DataSource.columns.count;
38 | }
39 |
40 | - (NSInteger)numberOfRowsInDataGridView:(DataGridView *)dataGridView {
41 | return self.dataSource.count;
42 | }
43 |
44 | - (NSString *)dataGridView:(DataGridView *)dataGridView titleForHeaderForColumn:(NSInteger)column {
45 | return F1DataSource.columnsTitles[column];
46 | }
47 |
48 | - (NSString *)dataGridView:(DataGridView *)dataGridView textForCellAtIndexPath:(NSIndexPath *)indexPath {
49 | NSString *fieldName = F1DataSource.columns[indexPath.dataGridColumn];
50 | return self.dataSource[indexPath.dataGridRow][fieldName];
51 | }
52 |
53 | - (DataGridViewColumnHeaderCell *)dataGridView:(DataGridView *)dataGridView viewForHeaderForColumn:(NSInteger)column {
54 | DataGridViewColumnHeaderCell *cell = [dataGridView dequeueReusableHeaderViewWithReuseIdentifier:@"DataGridViewColumnHeaderCell" forColumn:column];
55 | cell.title = [self dataGridView:dataGridView titleForHeaderForColumn:column];
56 | if (column == 1) {
57 | cell.borderRightWidth = 1 / UIScreen.mainScreen.scale;
58 | cell.borderRightColor = [UIColor colorWithWhite:0.72 alpha:1];
59 | } else {
60 | cell.borderRightWidth = 0;
61 | }
62 | return cell;
63 | }
64 |
65 | - (UICollectionViewCell *)dataGridView:(DataGridView *)dataGridView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
66 | DataGridViewContentCell *cell = (id)[dataGridView dequeueReusableCellWithReuseIdentifier:@"DataGridViewContentCell" forIndexPath:indexPath];
67 | cell.textLabel.text = [self dataGridView:dataGridView textForCellAtIndexPath:indexPath];
68 | switch (indexPath.dataGridColumn) {
69 | case 0:
70 | case 2:
71 | case 5:
72 | case 6:
73 | case 7:
74 | case 8:
75 | case 9:
76 | case 11:
77 | cell.textLabel.textAlignment = NSTextAlignmentRight;
78 | break;
79 | default:
80 | cell.textLabel.textAlignment = NSTextAlignmentLeft;
81 | break;
82 | }
83 | if (indexPath.dataGridColumn == 1) {
84 | cell.borderRightWidth = 1 / UIScreen.mainScreen.scale;
85 | cell.borderRightColor = [UIColor colorWithWhite:0.72 alpha:1];
86 | } else {
87 | cell.borderRightWidth = 0;
88 | }
89 | return cell;
90 | }
91 |
92 | #pragma mark - DataGridViewDelegate
93 |
94 | - (CGFloat)dataGridView:(DataGridView *)dataGridView widthForColumn:(NSInteger)column {
95 | return [F1DataSource.columnsWidths[column] floatValue];
96 | }
97 |
98 | - (BOOL)dataGridView:(DataGridView *)dataGridView shouldFloatColumn:(NSInteger)column {
99 | return column == 1;
100 | }
101 |
102 | - (BOOL)dataGridView:(DataGridView *)dataGridView shouldSortByColumn:(NSInteger)column {
103 | return YES;
104 | }
105 |
106 | - (void)dataGridView:(DataGridView *)dataGridView didSortByColumn:(NSInteger)column ascending:(BOOL)ascending {
107 | NSString *columnName = F1DataSource.columns[column];
108 | self.dataSource = [F1DataSource.stats sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
109 | return [obj1[columnName] compare:obj2[columnName]];
110 | }];
111 | [dataGridView reloadData];
112 | }
113 |
114 | @end
115 |
--------------------------------------------------------------------------------
/Source/CollectionViewDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CollectionViewDataSource.swift
3 | // Pods
4 | //
5 | // Created by Vladimir Lyukov on 30/07/15.
6 | //
7 | //
8 |
9 | import UIKit
10 |
11 |
12 | /**
13 | This class incapsulates logic for data source of UICollectionView used internally by DataGridView. You should not use this class directly.
14 | */
15 | open class CollectionViewDataSource: NSObject, UICollectionViewDataSource {
16 | open weak var dataGridView: DataGridView!
17 |
18 | public init(dataGridView: DataGridView) {
19 | self.dataGridView = dataGridView
20 | super.init()
21 | }
22 |
23 | // MARK: - UICollectionViewDataSource
24 |
25 | open func numberOfSections(in collectionView: UICollectionView) -> Int {
26 | if let numberOfRows = dataGridView?.dataSource?.numberOfRowsInDataGridView(dataGridView) {
27 | return numberOfRows
28 | }
29 | return 0
30 | }
31 |
32 | open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
33 | return dataGridView.dataSource?.numberOfColumnsInDataGridView(dataGridView) ?? 0
34 | }
35 |
36 | open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
37 | guard let dataGridDataSource = dataGridView.dataSource else {
38 | fatalError("dataGridView.dataSource unexpectedly nil")
39 | }
40 | if let cell = dataGridDataSource.dataGridView?(dataGridView, cellForItemAtIndexPath: indexPath) {
41 | return cell
42 | } else {
43 | let cell = dataGridView.dequeueReusableCellWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultCell, forIndexPath: indexPath) as! DataGridViewContentCell
44 | cell.textLabel.text = dataGridDataSource.dataGridView?(dataGridView, textForCellAtIndexPath: indexPath) ?? ""
45 | return cell
46 | }
47 | }
48 |
49 | open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
50 | guard let dataGridKind = DataGridView.SupplementaryViewKind(rawValue: kind) else {
51 | fatalError("Unexpected supplementary view kind: \(kind)")
52 | }
53 |
54 | switch dataGridKind {
55 | case .ColumnHeader: return columnHeaderViewForIndexPath(indexPath)
56 | case .RowHeader: return rowHeaderViewForIndexPath(indexPath)
57 | case .CornerHeader: return cornerHeaderViewForIndexPath(indexPath)
58 | }
59 | }
60 |
61 | // MARK: - Custom methods
62 | open func columnHeaderViewForIndexPath(_ indexPath: IndexPath) -> DataGridViewColumnHeaderCell{
63 | guard let dataGridDataSource = dataGridView.dataSource else {
64 | fatalError("dataGridView.dataSource unexpectedly nil")
65 | }
66 | let column = indexPath.index
67 | if let view = dataGridDataSource.dataGridView?(dataGridView, viewForHeaderForColumn: column) {
68 | return view
69 | }
70 | let cell = dataGridView.dequeueReusableHeaderViewWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultColumnHeader, forColumn: column)
71 | cell.configureForDataGridView(dataGridView, indexPath: indexPath)
72 | cell.title = dataGridDataSource.dataGridView?(dataGridView, titleForHeaderForColumn: column) ?? ""
73 | return cell
74 | }
75 |
76 | open func rowHeaderViewForIndexPath(_ indexPath: IndexPath) -> DataGridViewRowHeaderCell{
77 | guard let dataGridDataSource = dataGridView.dataSource else {
78 | fatalError("dataGridView.dataSource unexpectedly nil")
79 | }
80 | let row = indexPath.index
81 | if let view = dataGridDataSource.dataGridView?(dataGridView, viewForHeaderForRow: row) {
82 | return view
83 | }
84 | let cell = dataGridView.dequeueReusableHeaderViewWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultRowHeader, forRow: row)
85 | cell.title = dataGridDataSource.dataGridView?(dataGridView, titleForHeaderForRow: row) ?? ""
86 | return cell
87 | }
88 |
89 | open func cornerHeaderViewForIndexPath(_ indexPath: IndexPath) -> DataGridViewCornerHeaderCell {
90 | let cell = dataGridView.dequeueReusableCornerHeaderViewWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultRowHeader)
91 | return cell
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Example/GlyuckDataGrid.xcodeproj/xcshareddata/xcschemes/GlyuckDataGrid-Example.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
47 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
65 |
66 |
67 |
68 |
78 |
80 |
86 |
87 |
88 |
89 |
90 |
91 |
97 |
99 |
105 |
106 |
107 |
108 |
110 |
111 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/Example/GlyuckDataGrid/Examples/SpreadSheet/SpreadSheetViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SpreadSheetViewController.swift
3 | // GlyuckDataGrid
4 | //
5 | // Created by Vladimir Lyukov on 16/11/15.
6 | // Copyright © 2015 CocoaPods. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import GlyuckDataGrid
11 |
12 |
13 | class SpreadSheetViewController: UIViewController, DataGridViewDataSource, DataGridViewDelegate, SpreadSheetCellDelegate {
14 | enum Colors {
15 | static let border = UIColor.lightGray
16 | static let headerBackground = UIColor(red: 0.95, green: 0.95, blue: 0.95, alpha: 1)
17 | }
18 | enum Constants {
19 | static let numberOfRows = 99
20 | static let numberOfLetters = 26
21 | static let charCodeForA = 65
22 | }
23 | let cellReuseIdentifier = "DataCell"
24 | var dataArray: [[String]] = [[String]](repeating: [String](repeating: "", count: Constants.numberOfLetters), count: Constants.numberOfRows)
25 |
26 | @IBOutlet weak var dataGridView: DataGridView!
27 |
28 | static override func initialize() {
29 | super.initialize()
30 |
31 | let dataGridAppearance = DataGridView.glyuck_appearanceWhenContained(in: self)!
32 | dataGridAppearance.row1BackgroundColor = nil
33 | dataGridAppearance.row2BackgroundColor = nil
34 |
35 | let cornerHeaderAppearance = DataGridViewCornerHeaderCell.glyuck_appearanceWhenContained(in: self)!
36 | cornerHeaderAppearance.backgroundColor = Colors.headerBackground
37 | cornerHeaderAppearance.borderLeftWidth = 1 / UIScreen.main.scale
38 | cornerHeaderAppearance.borderTopWidth = 1 / UIScreen.main.scale
39 | cornerHeaderAppearance.borderRightWidth = 1 / UIScreen.main.scale
40 | cornerHeaderAppearance.borderBottomWidth = 1 / UIScreen.main.scale
41 | cornerHeaderAppearance.borderLeftColor = Colors.border
42 | cornerHeaderAppearance.borderTopColor = Colors.border
43 | cornerHeaderAppearance.borderRightColor = Colors.border
44 | cornerHeaderAppearance.borderBottomColor = Colors.border
45 |
46 | let rowHeaderAppearance = DataGridViewRowHeaderCell.glyuck_appearanceWhenContained(in :self)!
47 | rowHeaderAppearance.backgroundColor = Colors.headerBackground
48 | rowHeaderAppearance.borderLeftWidth = 1 / UIScreen.main.scale
49 | rowHeaderAppearance.borderBottomWidth = 1 / UIScreen.main.scale
50 | rowHeaderAppearance.borderRightWidth = 1 / UIScreen.main.scale
51 | rowHeaderAppearance.borderLeftColor = Colors.border
52 | rowHeaderAppearance.borderBottomColor = Colors.border
53 | rowHeaderAppearance.borderRightColor = Colors.border
54 |
55 | let rowHeaderLabelAppearane = UILabel.glyuck_appearanceWhenContained(in: self, class2: DataGridViewRowHeaderCell.self)!
56 | rowHeaderLabelAppearane.appearanceTextAlignment = .right
57 |
58 | let columnHeaderAppearance = DataGridViewColumnHeaderCell.glyuck_appearanceWhenContained(in: self)!
59 | columnHeaderAppearance.backgroundColor = Colors.headerBackground
60 | columnHeaderAppearance.borderTopWidth = 1 / UIScreen.main.scale
61 | columnHeaderAppearance.borderBottomWidth = 1 / UIScreen.main.scale
62 | columnHeaderAppearance.borderRightWidth = 1 / UIScreen.main.scale
63 | columnHeaderAppearance.borderTopColor = Colors.border
64 | columnHeaderAppearance.borderBottomColor = Colors.border
65 | columnHeaderAppearance.borderRightColor = Colors.border
66 |
67 | let cellAppearance = DataGridViewContentCell.glyuck_appearanceWhenContained(in: self)!
68 | cellAppearance.borderRightWidth = 1 / UIScreen.main.scale
69 | cellAppearance.borderRightColor = UIColor(white: 0.73, alpha: 1)
70 | cellAppearance.borderBottomWidth = 1 / UIScreen.main.scale
71 | cellAppearance.borderBottomColor = UIColor(white: 0.73, alpha: 1)
72 |
73 | columnHeaderAppearance.backgroundColor = UIColor(white: 0.95, alpha: 1)
74 | let labelAppearance = UILabel.glyuck_appearanceWhenContained(in: self)!
75 | labelAppearance.appearanceFont = UIFont.systemFont(ofSize: 12, weight: UIFontWeightLight)
76 | labelAppearance.appearanceTextAlignment = .center
77 | }
78 |
79 | override func viewDidLoad() {
80 | super.viewDidLoad()
81 |
82 | dataGridView.columnHeaderHeight = 40
83 | dataGridView.rowHeaderWidth = 40
84 | dataGridView.rowHeight = 44
85 |
86 | dataGridView.dataSource = self
87 | dataGridView.delegate = self
88 |
89 | dataGridView.registerNib(UINib(nibName: "SpreadSheetCell", bundle: nil), forCellWithReuseIdentifier: cellReuseIdentifier)
90 | }
91 |
92 | // MARK: DataGridViewDataSource
93 |
94 | func numberOfColumnsInDataGridView(_ dataGridView: DataGridView) -> Int {
95 | return Constants.numberOfLetters
96 | }
97 |
98 | func numberOfRowsInDataGridView(_ dataGridView: DataGridView) -> Int {
99 | return Constants.numberOfRows
100 | }
101 |
102 | func dataGridView(_ dataGridView: DataGridView, titleForHeaderForColumn column: Int) -> String {
103 | return String(Character(UnicodeScalar(Constants.charCodeForA + column)!))
104 | }
105 |
106 | func dataGridView(_ dataGridView: DataGridView, titleForHeaderForRow row: Int) -> String {
107 | return String(row + 1)
108 | }
109 |
110 | func dataGridView(_ dataGridView: DataGridView, cellForItemAtIndexPath indexPath: IndexPath) -> UICollectionViewCell {
111 | let cell = dataGridView.dequeueReusableCellWithReuseIdentifier(cellReuseIdentifier, forIndexPath: indexPath) as! SpreadSheetCell
112 | cell.delegate = self
113 | cell.border.bottomWidth = 1 / UIScreen.main.scale
114 | cell.border.rightWidth = 1 / UIScreen.main.scale
115 | cell.border.bottomColor = Colors.border
116 | cell.border.rightColor = Colors.border
117 | cell.configureWithData(dataArray[indexPath.dataGridRow][indexPath.dataGridColumn], forIndexPath: indexPath)
118 | return cell
119 | }
120 |
121 | // MARK: DataGridViewDelegate
122 |
123 | func dataGridView(_ dataGridView: DataGridView, widthForColumn column: Int) -> CGFloat {
124 | return 60
125 | }
126 |
127 | func dataGridView(_ dataGridView: DataGridView, shouldSelectRow row: Int) -> Bool {
128 | return false
129 | }
130 |
131 | // MARK: SpreadSheetCellDelegate
132 |
133 | func spreadSheetCell(_ cell: SpreadSheetCell, didUpdateData data: String, atIndexPath indexPath: IndexPath) {
134 | dataArray[indexPath.dataGridRow][indexPath.dataGridColumn] = data
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/Example/Tests/CollectionViewDelegateSpec.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CollectionViewDelegateSpec.swift
3 | //
4 | // Created by Vladimir Lyukov on 17/08/15.
5 | //
6 |
7 | import Foundation
8 |
9 | import Quick
10 | import Nimble
11 | import GlyuckDataGrid
12 |
13 |
14 | class CollectionViewDelegateSpec: QuickSpec {
15 | override func spec() {
16 | var dataGridView: DataGridView!
17 | var stubDataSource: StubDataGridViewDataSource!
18 | var stubDelegate: StubDataGridViewDelegate!
19 |
20 | var sut: CollectionViewDelegate!
21 |
22 | beforeEach {
23 | dataGridView = DataGridView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
24 | stubDataSource = StubDataGridViewDataSource()
25 | stubDelegate = StubDataGridViewDelegate()
26 | dataGridView.dataSource = stubDataSource
27 | dataGridView.delegate = stubDelegate
28 |
29 | sut = dataGridView.collectionViewDelegate
30 | }
31 |
32 | describe("collectionView:didTapHeaderForColumn:") {
33 | it("should change dataGridView.sortColumn and sortAscending when sorting enabled") {
34 | stubDelegate.shouldSortByColumnBlock = { (column) in return true }
35 | expect(dataGridView.sortColumn).to(beNil())
36 |
37 | // when
38 | sut.collectionView(dataGridView.collectionView, didTapHeaderForColumn: 0)
39 | // then
40 | expect(dataGridView.sortColumn) == 0
41 | expect(dataGridView.sortAscending).to(beTrue())
42 |
43 | // when
44 | sut.collectionView(dataGridView.collectionView, didTapHeaderForColumn: 1)
45 | // then
46 | expect(dataGridView.sortColumn) == 1
47 | expect(dataGridView.sortAscending).to(beTrue())
48 |
49 | // when
50 | sut.collectionView(dataGridView.collectionView, didTapHeaderForColumn: 1)
51 | // then
52 | expect(dataGridView.sortColumn) == 1
53 | expect(dataGridView.sortAscending).to(beFalse())
54 | }
55 |
56 | it("should do nothing when sorting is disabled") {
57 | expect(dataGridView.sortColumn).to(beNil())
58 |
59 | // given
60 | stubDelegate.shouldSortByColumnBlock = { (column) in return false }
61 | // when
62 | sut.collectionView(dataGridView.collectionView, didTapHeaderForColumn: 0)
63 | // then
64 | expect(dataGridView.sortColumn).to(beNil())
65 | }
66 |
67 | it("should do nothing when delegate doesnt implement shouldSortByColumn:") {
68 | // given
69 | dataGridView.delegate = nil
70 | // when
71 | sut.collectionView(dataGridView.collectionView, didTapHeaderForColumn: 0)
72 | // then
73 | expect(dataGridView.sortColumn).to(beNil())
74 | }
75 | }
76 |
77 | describe("collectionView:didHighlightItemAtIndexPath:") {
78 | it("should highlight whole row") {
79 | let row = 1
80 | let indexPath = IndexPath(item: 2, section: row + 1)
81 | let collectionView = dataGridView.collectionView
82 | collectionView.layoutSubviews() // Otherwise collectionView.cellForItemAtIndexPath won't work
83 | // when
84 | sut.collectionView(collectionView, didHighlightItemAt: indexPath)
85 |
86 | // then
87 | for i in 0.. DataGridViewColumnHeaderCell? {
53 | let indexPath = IndexPath(index: column)
54 | let view = sut.collectionView(dataGridView.collectionView, viewForSupplementaryElementOfKind: DataGridView.SupplementaryViewKind.ColumnHeader.rawValue, at: indexPath)
55 | return view as? DataGridViewColumnHeaderCell
56 | }
57 |
58 | context("when dataGridView:viewForHeaderForColumn: is implemented") {
59 | var stubDataSourceCustomCell: StubDataGridViewDataSourceCustomCell!
60 | beforeEach {
61 | stubDataSourceCustomCell = StubDataGridViewDataSourceCustomCell()
62 | dataGridView.dataSource = stubDataSourceCustomCell
63 | dataGridView.reloadData()
64 | dataGridView.layoutIfNeeded()
65 | }
66 |
67 | it("should return view created by delegate") {
68 | // given
69 | let expectedView = dataGridView.dequeueReusableHeaderViewWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultColumnHeader, forColumn: 1)
70 | stubDataSourceCustomCell.viewForColumnHeaderBlock = { dataGridView, column in
71 | expectedView.tag = column
72 | return expectedView
73 | }
74 | // when
75 | let view = headerCell(forColumn: 1)
76 | // then
77 | expect(view) == expectedView
78 | expect(view?.tag) == 1
79 | }
80 | }
81 |
82 | it("should return DataGridViewColumnHeaderCell for 0 section") {
83 | let cell = headerCell(forColumn: 0)
84 | expect(cell).to(beAKindOf(DataGridViewColumnHeaderCell.self))
85 | }
86 |
87 | it("should configure first header cell with corresponding text") {
88 | let cell = headerCell(forColumn: 0)
89 | expect(cell?.title) == "Title for column 0"
90 | }
91 |
92 | it("should configure second header cell with corresponding text") {
93 | let cell = headerCell(forColumn: 1)
94 | expect(cell?.title) == "Title for column 1"
95 | }
96 | }
97 |
98 | context("for row headers") {
99 | func headerCell(forRow row: Int) -> DataGridViewRowHeaderCell? {
100 | let indexPath = IndexPath(item: 0, section: row)
101 | let view = sut.collectionView(dataGridView.collectionView, viewForSupplementaryElementOfKind: DataGridView.SupplementaryViewKind.RowHeader.rawValue, at: indexPath)
102 | return view as? DataGridViewRowHeaderCell
103 | }
104 |
105 | context("when dataGridView:viewForHeaderForRow: is implemented") {
106 | var stubDataSourceCustomCell: StubDataGridViewDataSourceCustomCell!
107 | beforeEach {
108 | stubDataSourceCustomCell = StubDataGridViewDataSourceCustomCell()
109 | dataGridView.dataSource = stubDataSourceCustomCell
110 | dataGridView.reloadData()
111 | dataGridView.layoutIfNeeded()
112 | }
113 |
114 | it("should return view created by delegate") {
115 | // given
116 | let expectedView = dataGridView.dequeueReusableHeaderViewWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultRowHeader, forRow: 1)
117 | stubDataSourceCustomCell.viewForRowHeaderBlock = { dataGridView, row in
118 | expectedView.tag = row
119 | return expectedView
120 | }
121 | // when
122 | let view = headerCell(forRow: 1)
123 | // then
124 | expect(view) == expectedView
125 | expect(view?.tag) == 1
126 | }
127 | }
128 |
129 | it("should return DataGridViewColumnHeaderCell for 0 section") {
130 | let cell = headerCell(forRow: 0)
131 | expect(cell).to(beAKindOf(DataGridViewRowHeaderCell.self))
132 | }
133 |
134 | it("should configure first header cell with corresponding text") {
135 | let cell = headerCell(forRow: 0)
136 | expect(cell?.title) == "Title for row 0"
137 | }
138 |
139 | it("should configure second header cell with corresponding text") {
140 | let cell = headerCell(forRow: 1)
141 | expect(cell?.title) == "Title for row 1"
142 | }
143 | }
144 | context("for corner headers") {
145 | func cornerHeaderCell() -> DataGridViewCornerHeaderCell? {
146 | let indexPath = IndexPath(item: 0, section: 0)
147 | let view = sut.collectionView(dataGridView.collectionView, viewForSupplementaryElementOfKind: DataGridView.SupplementaryViewKind.CornerHeader.rawValue, at: indexPath)
148 | return view as? DataGridViewCornerHeaderCell
149 | }
150 |
151 | it("should return DataGridViewCornerHeaderCell") {
152 | let cell = cornerHeaderCell()
153 | expect(cell).to(beAKindOf(DataGridViewCornerHeaderCell.self))
154 | }
155 | }
156 | }
157 |
158 | describe("collectionView:cellForItemAtIndexPath:") {
159 | context("when dataGridView:cellForItemAtColumn:row: is implemented") {
160 | var stubDataSourceCustomCell: StubDataGridViewDataSourceCustomCell!
161 | beforeEach {
162 | stubDataSourceCustomCell = StubDataGridViewDataSourceCustomCell()
163 | dataGridView.dataSource = stubDataSourceCustomCell
164 | dataGridView.reloadData()
165 | dataGridView.layoutIfNeeded()
166 | }
167 |
168 | it("should return view created by delegate") {
169 | // given
170 | let expectedCell = dataGridView.dequeueReusableCellWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultCell, forIndexPath: IndexPath(item: 0, section: 0))
171 | stubDataSourceCustomCell.cellForItemBlock = { dataGridView, indexPath in
172 | expectedCell.tag = indexPath.dataGridColumn * 100 + indexPath.dataGridRow
173 | return expectedCell
174 | }
175 | // when
176 | let cell = sut.collectionView(dataGridView.collectionView, cellForItemAt: IndexPath(item: 2, section: 1))
177 | // then
178 | expect(cell) == expectedCell
179 | expect(cell.tag) == 201
180 | }
181 | }
182 |
183 | context("when dataGridView:cellForItemAtColumn:row: is not implemented") {
184 | it("should return DataGridViewContentCell for 1 section") {
185 | let cell = sut.collectionView(dataGridView.collectionView, cellForItemAt: IndexPath(item: 0, section: 1))
186 | expect(cell).to(beAKindOf(DataGridViewContentCell.self))
187 | }
188 |
189 | it("should configure cell 0,0 with corresponding text") {
190 | let cell = sut.collectionView(dataGridView.collectionView, cellForItemAt: IndexPath(item: 0, section: 0)) as! DataGridViewContentCell
191 | expect(cell.textLabel.text) == "Text for cell 0x0"
192 | }
193 |
194 | it("should configure cell 1,2 with corresponding text") {
195 | let cell = sut.collectionView(dataGridView.collectionView, cellForItemAt: IndexPath(item: 1, section: 2)) as! DataGridViewContentCell
196 | expect(cell.textLabel.text) == "Text for cell 1x2"
197 | }
198 | }
199 | }
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/Example/Tests/DataGridViewSpec.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataGridViewSpec.swift
3 | //
4 | // Created by Vladimir Lyukov on 30/07/15.
5 | //
6 |
7 | import Quick
8 | import Nimble
9 | import GlyuckDataGrid
10 |
11 |
12 | class DataGridViewSpec: QuickSpec {
13 | override func spec() {
14 | var sut: DataGridView!
15 | var stubDataSource: StubDataGridViewDataSource!
16 |
17 | beforeEach {
18 | stubDataSource = StubDataGridViewDataSource()
19 | sut = DataGridView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
20 | sut.dataSource = stubDataSource
21 | sut.collectionView.layoutIfNeeded()
22 | }
23 |
24 | describe("Registering/dequeuing cells") {
25 | context("zebra-striped tables") {
26 | it("should return transparent cells when row1BackgroundColor and row2BackgroundColor are nil") {
27 | let cell1 = sut.dequeueReusableCellWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultCell, forIndexPath: IndexPath(forColumn: 1, row: 0))
28 | let cell2 = sut.dequeueReusableCellWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultCell, forIndexPath: IndexPath(forColumn: 1, row: 1))
29 | expect(cell1.backgroundColor).to(beNil())
30 | expect(cell2.backgroundColor).to(beNil())
31 | }
32 |
33 | it("should return row1BackgroundColor for odd rows and row2BackgroundColor for even rows") {
34 | sut.row1BackgroundColor = UIColor.red
35 | sut.row2BackgroundColor = UIColor.green
36 | let cell1 = sut.dequeueReusableCellWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultCell, forIndexPath: IndexPath(forColumn: 1, row: 0))
37 | let cell2 = sut.dequeueReusableCellWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultCell, forIndexPath: IndexPath(forColumn: 1, row: 1))
38 | expect(cell1.backgroundColor) == sut.row1BackgroundColor
39 | expect(cell2.backgroundColor) == sut.row2BackgroundColor
40 | }
41 | }
42 |
43 | it("should register and dequeue cells") {
44 | sut.registerClass(DataGridViewContentCell.self, forCellWithReuseIdentifier: "MyIdentifier")
45 |
46 | let cell = sut.dequeueReusableCellWithReuseIdentifier("MyIdentifier", forIndexPath: IndexPath(forColumn: 0, row: 0))
47 |
48 | expect(cell).notTo(beNil())
49 | }
50 |
51 | it("should register and dequeue column headers") {
52 | sut.registerClass(DataGridViewColumnHeaderCell.self, forHeaderOfKind: .ColumnHeader, withReuseIdentifier: "MyIdentifier")
53 |
54 | let cell = sut.dequeueReusableHeaderViewWithReuseIdentifier("MyIdentifier", forColumn: 1)
55 |
56 | expect(cell).notTo(beNil())
57 | expect(cell.dataGridView) == sut
58 | expect(cell.indexPath) == IndexPath(index: 1)
59 | }
60 |
61 | it("should register and dequeue row headers") {
62 | sut.registerClass(DataGridViewRowHeaderCell.self, forHeaderOfKind: .RowHeader, withReuseIdentifier: "MyIdentifier")
63 |
64 | let cell = sut.dequeueReusableHeaderViewWithReuseIdentifier("MyIdentifier", forRow: 1)
65 |
66 | expect(cell).notTo(beNil())
67 | expect(cell.dataGridView) == sut
68 | expect(cell.indexPath) == IndexPath(index: 1)
69 | }
70 |
71 | it("should register and dequeue corner headers") {
72 | sut.registerClass(DataGridViewCornerHeaderCell.self, forHeaderOfKind: .CornerHeader, withReuseIdentifier: "MyIdentifier")
73 |
74 | let cell = sut.dequeueReusableCornerHeaderViewWithReuseIdentifier("MyIdentifier")
75 |
76 | expect(cell).notTo(beNil())
77 | expect(cell.dataGridView) == sut
78 | expect(cell.indexPath) == IndexPath(index: 0)
79 | }
80 | }
81 |
82 | describe("collectionView") {
83 | it("should not be nil") {
84 | expect(sut.collectionView).notTo(beNil())
85 | }
86 |
87 | it("should be subview of dataGridView") {
88 | expect(sut.collectionView.superview) === sut
89 | }
90 |
91 | it("should resize along with dataGridView") {
92 | sut.collectionView.layoutIfNeeded() // Ensure text label is initialized when tests are started
93 |
94 | sut.frame = CGRect(x: 0, y: 0, width: sut.frame.width * 2, height: sut.frame.height / 2)
95 | sut.layoutIfNeeded()
96 | expect(sut.collectionView.frame) == sut.frame
97 | }
98 |
99 | it("should register DataGridViewContentCell as default cell") {
100 | let cell = sut.dequeueReusableCellWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultCell, forIndexPath: IndexPath(forColumn: 0, row: 0)) as? DataGridViewContentCell
101 | expect(cell).notTo(beNil())
102 | }
103 |
104 | it("should register DataGridViewColumnHeaderCell as default column header") {
105 | let header = sut.dequeueReusableHeaderViewWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultColumnHeader, forColumn: 0)
106 | expect(header).notTo(beNil())
107 | }
108 |
109 | it("should set isSorted and iSortedAsc for column headers") {
110 | let header1 = sut.dequeueReusableHeaderViewWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultColumnHeader, forColumn: 0)
111 | expect(header1.isSorted).to(beFalse())
112 |
113 | sut.setSortColumn(0, ascending: false)
114 |
115 | let header2 = sut.dequeueReusableHeaderViewWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultColumnHeader, forColumn: 0)
116 | expect(header2.isSorted).to(beTrue())
117 | expect(header2.isSortedAsc).to(beFalse())
118 | }
119 |
120 | it("should register DataGridViewRowHeaderCell as default row header") {
121 | let header = sut.dequeueReusableHeaderViewWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultRowHeader, forRow: 0)
122 | expect(header).notTo(beNil())
123 | }
124 |
125 | it("should register DataGridViewCornerHeaderCell as default row header") {
126 | let header = sut.dequeueReusableCornerHeaderViewWithReuseIdentifier(DataGridView.ReuseIdentifiers.defaultCornerHeader)
127 | expect(header).notTo(beNil())
128 | }
129 |
130 | it("should have CollectionViewDataSource as dataSource") {
131 | let dataSource = sut.collectionView.dataSource as? CollectionViewDataSource
132 | expect(dataSource).notTo(beNil())
133 | expect(dataSource) == sut.collectionViewDataSource
134 | expect(dataSource?.dataGridView) == sut
135 | }
136 |
137 | it("should have CollectionViewDelegate as delegate") {
138 | let delegate = sut.collectionView.delegate as? CollectionViewDelegate
139 | expect(delegate).notTo(beNil())
140 | expect(delegate) == sut.collectionViewDelegate
141 | expect(delegate?.dataGridView) == sut
142 | }
143 |
144 | it("should have transparent background") {
145 | expect(sut.collectionView.backgroundColor) == UIColor.clear
146 | }
147 |
148 | describe("layout") {
149 | it("should be instance of DataGridViewLayout") {
150 | expect(sut.collectionView.collectionViewLayout).to(beAKindOf(DataGridViewLayout.self))
151 | }
152 | it("should have dataGridView set") {
153 | let layout = sut.collectionView.collectionViewLayout as? DataGridViewLayout
154 | expect(layout?.dataGridView) === sut
155 | }
156 | it("should have collectionView and dataGridView properties set") {
157 | let layout = sut.collectionView.collectionViewLayout as? DataGridViewLayout
158 | expect(layout).notTo(beNil())
159 | if let layout = layout {
160 | expect(layout.dataGridView) === sut
161 | expect(layout.collectionView) === sut.collectionView
162 | }
163 | }
164 | }
165 | }
166 |
167 | describe("numberOfColumns") {
168 | it("should return value provided by dataSource") {
169 | stubDataSource.numberOfColumns = 7
170 | sut.dataSource = stubDataSource
171 | expect(sut.numberOfColumns()) == stubDataSource.numberOfColumns
172 | }
173 | it("should return 0 if dataSource is nil") {
174 | sut.dataSource = nil
175 | expect(sut.numberOfColumns()) == 0
176 | }
177 | }
178 |
179 | describe("numberOfRows") {
180 | it("should return value provided by dataSource") {
181 | stubDataSource.numberOfRows = 7
182 | sut.dataSource = stubDataSource
183 | expect(sut.numberOfRows()) == stubDataSource.numberOfRows
184 | }
185 | it("should return 0 if dataSource is nil") {
186 | sut.dataSource = nil
187 | expect(sut.numberOfRows()) == 0
188 | }
189 | }
190 |
191 | describe("selectRow:animated:") {
192 | it("should select all items in corresponding section") {
193 | let row = 1
194 | sut.selectRow(row, animated: false)
195 |
196 | expect(sut.collectionView.indexPathsForSelectedItems?.count) == sut.numberOfColumns()
197 | for i in 0.. CGFloat {
32 | return dataGridView?.delegate?.dataGridView?(dataGridView!, heightForRow: row) ?? dataGridView.rowHeight
33 | }
34 |
35 | open func widthForColumn(_ column: Int) -> CGFloat {
36 | if let width = dataGridView?.delegate?.dataGridView?(dataGridView!, widthForColumn: column) {
37 | return width
38 | }
39 | if let dataGridView = dataGridView, let dataSource = dataGridView.dataSource {
40 | let exactWidth = (dataGridView.frame.width - dataGridView.rowHeaderWidth) / CGFloat(dataSource.numberOfColumnsInDataGridView(dataGridView))
41 | let exactStart = Array(0.. CGFloat {
49 | return dataGridView.rowHeaderWidth
50 | }
51 |
52 | open func heightForSectionHeader() -> CGFloat {
53 | return dataGridView.columnHeaderHeight
54 | }
55 |
56 | // MARK: UICollectionViewLayout
57 |
58 | open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
59 | let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
60 | let x = Array(0..<(indexPath as NSIndexPath).row).reduce(widthForRowHeader()) { $0 + widthForColumn($1)}
61 | let y = Array(0..<(indexPath as NSIndexPath).section).reduce(heightForSectionHeader()) { $0 + heightForRow($1)}
62 | let width = widthForColumn((indexPath as NSIndexPath).row)
63 | let height = heightForRow((indexPath as NSIndexPath).section)
64 | attributes.frame = CGRect(
65 | x: max(0, x),
66 | y: max(0, y),
67 | width: width,
68 | height: height
69 | )
70 | if dataGridView?.delegate?.dataGridView?(dataGridView!, shouldFloatColumn: (indexPath as NSIndexPath).row) == true {
71 | let scrollOffsetX = dataGridView.collectionView.contentOffset.x + collectionView!.contentInset.left
72 | let floatWidths = Array(0..<(indexPath as NSIndexPath).row).reduce(CGFloat(0)) {
73 | if dataGridView?.delegate?.dataGridView?(dataGridView!, shouldFloatColumn: $1) == true {
74 | return $0 + widthForColumn($1)
75 | } else {
76 | return $0
77 | }
78 | }
79 | attributes.frame.origin.x = max(scrollOffsetX + floatWidths, attributes.frame.origin.x)
80 | attributes.zIndex += 1
81 | }
82 |
83 | return attributes
84 | }
85 |
86 | open override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
87 | guard let elementKind = DataGridView.SupplementaryViewKind(rawValue: elementKind) else {
88 | return nil
89 | }
90 | return layoutAttributesForSupplementaryViewOfKind(elementKind, atIndexPath: indexPath)
91 | }
92 |
93 | open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
94 | var items = [Int]()
95 | var sections = [Int]()
96 |
97 | var x:CGFloat = widthForRowHeader()
98 | for i in (0..= rect.maxX {
100 | break
101 | }
102 |
103 | let nextX = x + widthForColumn(i)
104 | if x - widthForRowHeader() >= rect.minX || nextX - widthForRowHeader() > rect.minX ||
105 | dataGridView?.delegate?.dataGridView?(dataGridView!, shouldFloatColumn: i) == true {
106 | items.append(i)
107 | }
108 | x = nextX
109 | }
110 |
111 | let headerHeight = heightForSectionHeader()
112 | var y: CGFloat = heightForSectionHeader()
113 | for i in (0..= rect.maxY {
115 | break
116 | }
117 |
118 | let nextY = y + heightForRow(0)
119 | if y - headerHeight >= rect.minY || nextY - headerHeight > rect.minY {
120 | sections.append(i)
121 | }
122 | y = nextY
123 | }
124 |
125 | var result = [UICollectionViewLayoutAttributes]()
126 | // Cells
127 | for item in items {
128 | for section in sections {
129 | let indexPath = IndexPath(item: item, section: section)
130 | result.append(layoutAttributesForItem(at: indexPath)!)
131 | }
132 | }
133 | // Column headers
134 | for item in items {
135 | let headerIndexPath = IndexPath(index: item)
136 | if let headerAttributes = layoutAttributesForSupplementaryViewOfKind(.ColumnHeader, atIndexPath: headerIndexPath) {
137 | result.append(headerAttributes)
138 | }
139 | }
140 | // Row headers
141 | if widthForRowHeader() > 0 {
142 | for section in sections {
143 | let rowHeaderIndexPath = IndexPath(index: section)
144 | if let rowHeaderAttributes = layoutAttributesForSupplementaryViewOfKind(.RowHeader, atIndexPath: rowHeaderIndexPath) {
145 | result.append(rowHeaderAttributes)
146 | }
147 | }
148 | }
149 | // Corner header
150 | if widthForRowHeader() > 0 && heightForSectionHeader() > 0 {
151 | let cornerHeaderIndexPath = IndexPath(index: 0)
152 | if let cornerHeaderAttributes = layoutAttributesForSupplementaryViewOfKind(.CornerHeader, atIndexPath: cornerHeaderIndexPath) {
153 | result.append(cornerHeaderAttributes)
154 | }
155 | }
156 |
157 | return result
158 | }
159 |
160 | open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
161 | return true
162 | }
163 |
164 | open override var collectionViewContentSize : CGSize {
165 | let width = Array(0.. UICollectionViewLayoutAttributes? {
172 | switch elementKind {
173 | case .ColumnHeader: return layoutAttributesForColumnHeaderViewAtIndexPath(indexPath)
174 | case .RowHeader: return layoutAttributesForRowHeaderViewAtIndexPath(indexPath)
175 | case .CornerHeader: return layoutAttributesForCornerHeaderViewAtIndexPath(indexPath)
176 | }
177 | }
178 |
179 | open func layoutAttributesForColumnHeaderViewAtIndexPath(_ indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
180 | let attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: DataGridView.SupplementaryViewKind.ColumnHeader.rawValue, with: indexPath)
181 | let x = Array(0.. UICollectionViewLayoutAttributes? {
208 | let attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: DataGridView.SupplementaryViewKind.RowHeader.rawValue, with: indexPath)
209 | let x = collectionView!.contentInset.left + dataGridView.collectionView.contentOffset.x
210 | let y = Array(0.. UICollectionViewLayoutAttributes? {
224 | let attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: DataGridView.SupplementaryViewKind.CornerHeader.rawValue, with: indexPath)
225 | let x = collectionView!.contentInset.left + dataGridView.collectionView.contentOffset.x
226 | let y = collectionView!.contentInset.top + dataGridView.collectionView.contentOffset.y
227 | let width = widthForRowHeader()
228 | let height = heightForSectionHeader()
229 | attributes.frame = CGRect(
230 | x: max(0, x),
231 | y: max(0, y),
232 | width: width,
233 | height: height
234 | )
235 | attributes.zIndex = 5
236 | return attributes
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/Example/GlyuckDataGrid/F1DataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // F1DataSource.swift
3 | //
4 | // Created by Vladimir Lyukov on 14/08/15.
5 | //
6 |
7 | import Foundation
8 | import UIKit
9 |
10 |
11 | class F1DataSource: NSObject {
12 | static let stats:[[String:String]] = [
13 | ["season": "1950", "driver": "Giuseppe Farina", "age": "44", "team": "Alfa Romeo", "engine": "Alfa Romeo", "poles": "2", "wins": "3", "podiums": "3", "fastest_laps": "3", "points": "30", "clinched": "Race 7 of 7", "point_margin": "3"],
14 | ["season": "1951", "driver": "Juan Manuel Fangio", "age": "40", "team": "Alfa Romeo", "engine": "Alfa Romeo", "poles": "4", "wins": "3", "podiums": "5", "fastest_laps": "5", "points": "31", "clinched": "Race 8 of 8", "point_margin": "6"],
15 | ["season": "1952", "driver": "Alberto Ascari", "age": "34", "team": "Ferrari", "engine": "Ferrari", "poles": "5", "wins": "6", "podiums": "6", "fastest_laps": "6", "points": "36", "clinched": "Race 6 of 8", "point_margin": "12"],
16 | ["season": "1953", "driver": "Alberto Ascari", "age": "35", "team": "Ferrari", "engine": "Ferrari", "poles": "6", "wins": "5", "podiums": "5", "fastest_laps": "4", "points": "34.5", "clinched": "Race 8 of 9", "point_margin": "6.5"],
17 | ["season": "1954", "driver": "Juan Manuel Fangio", "age": "43", "team": "Maserati2", "engine": "Maserati", "poles": "5", "wins": "6", "podiums": "7", "fastest_laps": "3", "points": "42", "clinched": "Race 7 of 9", "point_margin": "16.86"],
18 | ["season": "1955", "driver": "Juan Manuel Fangio", "age": "44", "team": "Mercedes", "engine": "Mercedes", "poles": "3", "wins": "4", "podiums": "5", "fastest_laps": "3", "points": "40", "clinched": "Race 6 of 7", "point_margin": "16.5"],
19 | ["season": "1956", "driver": "Juan Manuel Fangio", "age": "45", "team": "Ferrari", "engine": "Ferrari", "poles": "6", "wins": "3", "podiums": "5", "fastest_laps": "4", "points": "30", "clinched": "Race 8 of 8", "point_margin": "3"],
20 | ["season": "1957", "driver": "Juan Manuel Fangio", "age": "46", "team": "Maserati", "engine": "Maserati", "poles": "4", "wins": "4", "podiums": "6", "fastest_laps": "2", "points": "40", "clinched": "Race 6 of 8", "point_margin": "15"],
21 | ["season": "1958", "driver": "Mike Hawthorn", "age": "29", "team": "Ferrari", "engine": "Ferrari", "poles": "4", "wins": "1", "podiums": "7", "fastest_laps": "5", "points": "42", "clinched": "Race 11 of 11", "point_margin": "1"],
22 | ["season": "1959", "driver": "Jack Brabham", "age": "33", "team": "Cooper", "engine": "Climax", "poles": "1", "wins": "2", "podiums": "5", "fastest_laps": "1", "points": "31", "clinched": "Race 9 of 9", "point_margin": "4"],
23 | ["season": "1960", "driver": "Jack Brabham", "age": "34", "team": "Cooper", "engine": "Climax", "poles": "3", "wins": "5", "podiums": "5", "fastest_laps": "3", "points": "43", "clinched": "Race 8 of 10", "point_margin": "9"],
24 | ["season": "1961", "driver": "Phil Hill", "age": "34", "team": "Ferrari", "engine": "Ferrari", "poles": "5", "wins": "2", "podiums": "6", "fastest_laps": "2", "points": "34", "clinched": "Race 7 of 8", "point_margin": "1"],
25 | ["season": "1962", "driver": "Graham Hill", "age": "33", "team": "BRM", "engine": "BRM", "poles": "1", "wins": "4", "podiums": "6", "fastest_laps": "3", "points": "42", "clinched": "Race 9 of 9", "point_margin": "12"],
26 | ["season": "1963", "driver": "Jim Clark", "age": "27", "team": "Lotus", "engine": "Climax", "poles": "7", "wins": "7", "podiums": "9", "fastest_laps": "6", "points": "54", "clinched": "Race 7 of 10", "point_margin": "21"],
27 | ["season": "1964", "driver": "John Surtees", "age": "30", "team": "Ferrari", "engine": "Ferrari", "poles": "2", "wins": "2", "podiums": "6", "fastest_laps": "2", "points": "40", "clinched": "Race 10 of 10", "point_margin": "1"],
28 | ["season": "1965", "driver": "Jim Clark", "age": "29", "team": "Lotus", "engine": "Climax", "poles": "6", "wins": "6", "podiums": "6", "fastest_laps": "6", "points": "54", "clinched": "Race 7 of 10", "point_margin": "14"],
29 | ["season": "1966", "driver": "Jack Brabham", "age": "40", "team": "Brabham", "engine": "Repco", "poles": "3", "wins": "4", "podiums": "5", "fastest_laps": "1", "points": "42", "clinched": "Race 7 of 9", "point_margin": "14"],
30 | ["season": "1967", "driver": "Denny Hulme", "age": "31", "team": "Brabham", "engine": "Repco", "poles": "0", "wins": "2", "podiums": "8", "fastest_laps": "2", "points": "51", "clinched": "Race 11 of 11", "point_margin": "5"],
31 | ["season": "1968", "driver": "Graham Hill", "age": "39", "team": "Lotus", "engine": "Ford", "poles": "2", "wins": "3", "podiums": "6", "fastest_laps": "0", "points": "48", "clinched": "Race 12 of 12", "point_margin": "12"],
32 | ["season": "1969", "driver": "Jackie Stewart", "age": "30", "team": "Matra", "engine": "Ford", "poles": "2", "wins": "6", "podiums": "7", "fastest_laps": "5", "points": "63", "clinched": "Race 8 of 11", "point_margin": "26"],
33 | ["season": "1970", "driver": "Jochen Rindt", "age": "28", "team": "Lotus", "engine": "Ford", "poles": "3", "wins": "5", "podiums": "5", "fastest_laps": "1", "points": "45", "clinched": "Race 12 of 13", "point_margin": "5"],
34 | ["season": "1971", "driver": "Jackie Stewart", "age": "32", "team": "Tyrrell", "engine": "Ford", "poles": "6", "wins": "6", "podiums": "7", "fastest_laps": "3", "points": "62", "clinched": "Race 8 of 11", "point_margin": "29"],
35 | ["season": "1972", "driver": "Emerson Fittipaldi", "age": "25", "team": "Lotus", "engine": "Ford", "poles": "3", "wins": "5", "podiums": "8", "fastest_laps": "0", "points": "61", "clinched": "Race 10 of 12", "point_margin": "16"],
36 | ["season": "1973", "driver": "Jackie Stewart", "age": "34", "team": "Tyrrell", "engine": "Ford", "poles": "3", "wins": "5", "podiums": "8", "fastest_laps": "1", "points": "71", "clinched": "Race 13 of 15", "point_margin": "16"],
37 | ["season": "1974", "driver": "Emerson Fittipaldi", "age": "27", "team": "McLaren", "engine": "Ford", "poles": "2", "wins": "3", "podiums": "7", "fastest_laps": "0", "points": "55", "clinched": "Race 15 of 15", "point_margin": "3"],
38 | ["season": "1975", "driver": "Niki Lauda", "age": "26", "team": "Ferrari", "engine": "Ferrari", "poles": "9", "wins": "5", "podiums": "8", "fastest_laps": "2", "points": "64.5", "clinched": "Race 13 of 14", "point_margin": "19.5"],
39 | ["season": "1976", "driver": "James Hunt", "age": "29", "team": "McLaren", "engine": "Ford", "poles": "8", "wins": "6", "podiums": "8", "fastest_laps": "2", "points": "69", "clinched": "Race 16 of 16", "point_margin": "1"],
40 | ["season": "1977", "driver": "Niki Lauda", "age": "28", "team": "Ferrari", "engine": "Ferrari", "poles": "2", "wins": "3", "podiums": "10", "fastest_laps": "3", "points": "72", "clinched": "Race 15 of 17", "point_margin": "17"],
41 | ["season": "1978", "driver": "Mario Andretti", "age": "38", "team": "Lotus", "engine": "Ford", "poles": "8", "wins": "6", "podiums": "7", "fastest_laps": "3", "points": "64", "clinched": "Race 14 of 16", "point_margin": "13"],
42 | ["season": "1979", "driver": "Jody Scheckter", "age": "29", "team": "Ferrari", "engine": "Ferrari", "poles": "1", "wins": "3", "podiums": "6", "fastest_laps": "0", "points": "51", "clinched": "Race 13 of 15", "point_margin": "4"],
43 | ["season": "1980", "driver": "Alan Jones", "age": "34", "team": "Williams", "engine": "Ford", "poles": "3", "wins": "5", "podiums": "10", "fastest_laps": "5", "points": "67", "clinched": "Race 13 of 14", "point_margin": "13"],
44 | ["season": "1981", "driver": "Nelson Piquet", "age": "29", "team": "Brabham", "engine": "Ford", "poles": "4", "wins": "3", "podiums": "7", "fastest_laps": "1", "points": "50", "clinched": "Race 15 of 15", "point_margin": "1"],
45 | ["season": "1982", "driver": "Keke Rosberg", "age": "34", "team": "Williams", "engine": "Ford", "poles": "1", "wins": "1", "podiums": "6", "fastest_laps": "0", "points": "44", "clinched": "Race 16 of 16", "point_margin": "5"],
46 | ["season": "1983", "driver": "Nelson Piquet", "age": "31", "team": "Brabham", "engine": "BMW", "poles": "1", "wins": "3", "podiums": "8", "fastest_laps": "4", "points": "59", "clinched": "Race 15 of 15", "point_margin": "2"],
47 | ["season": "1984", "driver": "Niki Lauda", "age": "35", "team": "McLaren", "engine": "TAG", "poles": "0", "wins": "5", "podiums": "9", "fastest_laps": "5", "points": "72", "clinched": "Race 16 of 16", "point_margin": "0.5"],
48 | ["season": "1985", "driver": "Alain Prost", "age": "30", "team": "McLaren", "engine": "TAG", "poles": "2", "wins": "5", "podiums": "11", "fastest_laps": "5", "points": "73", "clinched": "Race 14 of 16", "point_margin": "20"],
49 | ["season": "1986", "driver": "Alain Prost", "age": "31", "team": "McLaren", "engine": "TAG", "poles": "1", "wins": "4", "podiums": "11", "fastest_laps": "2", "points": "72", "clinched": "Race 16 of 16", "point_margin": "2"],
50 | ["season": "1987", "driver": "Nelson Piquet", "age": "35", "team": "Williams", "engine": "Honda", "poles": "4", "wins": "3", "podiums": "11", "fastest_laps": "4", "points": "73", "clinched": "Race 15 of 16", "point_margin": "12"],
51 | ["season": "1988", "driver": "Ayrton Senna", "age": "28", "team": "McLaren", "engine": "Honda", "poles": "13", "wins": "8", "podiums": "11", "fastest_laps": "3", "points": "90", "clinched": "Race 15 of 16", "point_margin": "3"],
52 | ["season": "1989", "driver": "Alain Prost", "age": "34", "team": "McLaren", "engine": "Honda", "poles": "2", "wins": "4", "podiums": "11", "fastest_laps": "5", "points": "76", "clinched": "Race 15 of 16", "point_margin": "16"],
53 | ["season": "1990", "driver": "Ayrton Senna", "age": "30", "team": "McLaren", "engine": "Honda", "poles": "10", "wins": "6", "podiums": "11", "fastest_laps": "2", "points": "78", "clinched": "Race 15 of 16", "point_margin": "7"],
54 | ["season": "1991", "driver": "Ayrton Senna", "age": "31", "team": "McLaren", "engine": "Honda", "poles": "8", "wins": "7", "podiums": "12", "fastest_laps": "2", "points": "96", "clinched": "Race 15 of 16", "point_margin": "24"],
55 | ["season": "1992", "driver": "Nigel Mansell", "age": "39", "team": "Williams", "engine": "Renault", "poles": "14", "wins": "9", "podiums": "12", "fastest_laps": "8", "points": "108", "clinched": "Race 11 of 16", "point_margin": "52"],
56 | ["season": "1993", "driver": "Alain Prost", "age": "38", "team": "Williams", "engine": "Renault", "poles": "13", "wins": "7", "podiums": "12", "fastest_laps": "6", "points": "99", "clinched": "Race 14 of 16", "point_margin": "26"],
57 | ["season": "1994", "driver": "Michael Schumacher", "age": "25", "team": "Benetton", "engine": "Ford", "poles": "6", "wins": "8", "podiums": "10", "fastest_laps": "8", "points": "92", "clinched": "Race 16 of 16", "point_margin": "1"],
58 | ["season": "1995", "driver": "Michael Schumacher", "age": "26", "team": "Benetton", "engine": "Renault", "poles": "4", "wins": "9", "podiums": "11", "fastest_laps": "8", "points": "102", "clinched": "Race 15 of 17", "point_margin": "33"],
59 | ["season": "1996", "driver": "Damon Hill", "age": "36", "team": "Williams", "engine": "Renault", "poles": "9", "wins": "8", "podiums": "10", "fastest_laps": "5", "points": "97", "clinched": "Race 16 of 16", "point_margin": "19"],
60 | ["season": "1997", "driver": "Jacques Villeneuve", "age": "26", "team": "Williams", "engine": "Renault", "poles": "10", "wins": "7", "podiums": "8", "fastest_laps": "3", "points": "81", "clinched": "Race 17 of 17", "point_margin": "39"],
61 | ["season": "1998", "driver": "Mika Häkkinen", "age": "30", "team": "McLaren", "engine": "Mercedes", "poles": "9", "wins": "8", "podiums": "11", "fastest_laps": "6", "points": "100", "clinched": "Race 16 of 16", "point_margin": "14"],
62 | ["season": "1999", "driver": "Mika Häkkinen", "age": "31", "team": "McLaren", "engine": "Mercedes", "poles": "11", "wins": "5", "podiums": "10", "fastest_laps": "6", "points": "76", "clinched": "Race 16 of 16", "point_margin": "2"],
63 | ["season": "2000", "driver": "Michael Schumacher", "age": "31", "team": "Ferrari", "engine": "Ferrari", "poles": "9", "wins": "9", "podiums": "12", "fastest_laps": "2", "points": "108", "clinched": "Race 16 of 17", "point_margin": "19"],
64 | ["season": "2001", "driver": "Michael Schumacher", "age": "32", "team": "Ferrari", "engine": "Ferrari", "poles": "11", "wins": "9", "podiums": "14", "fastest_laps": "3", "points": "123", "clinched": "Race 13 of 17", "point_margin": "58"],
65 | ["season": "2002", "driver": "Michael Schumacher", "age": "33", "team": "Ferrari", "engine": "Ferrari", "poles": "7", "wins": "11", "podiums": "17", "fastest_laps": "7", "points": "144", "clinched": "Race 11 of 17", "point_margin": "67"],
66 | ["season": "2003", "driver": "Michael Schumacher", "age": "34", "team": "Ferrari", "engine": "Ferrari", "poles": "5", "wins": "6", "podiums": "8", "fastest_laps": "5", "points": "93", "clinched": "Race 16 of 16", "point_margin": "2"],
67 | ["season": "2004", "driver": "Michael Schumacher", "age": "35", "team": "Ferrari", "engine": "Ferrari", "poles": "8", "wins": "13", "podiums": "15", "fastest_laps": "10", "points": "148", "clinched": "Race 14 of 18", "point_margin": "34"],
68 | ["season": "2005", "driver": "Fernando Alonso", "age": "24", "team": "Renault", "engine": "Renault", "poles": "6", "wins": "7", "podiums": "15", "fastest_laps": "2", "points": "133", "clinched": "Race 17 of 19", "point_margin": "21"],
69 | ["season": "2006", "driver": "Fernando Alonso", "age": "25", "team": "Renault", "engine": "Renault", "poles": "6", "wins": "7", "podiums": "14", "fastest_laps": "5", "points": "134", "clinched": "Race 18 of 18", "point_margin": "13"],
70 | ["season": "2007", "driver": "Kimi Räikkönen", "age": "28", "team": "Ferrari", "engine": "Ferrari", "poles": "3", "wins": "6", "podiums": "12", "fastest_laps": "6", "points": "110", "clinched": "Race 17 of 17", "point_margin": "1"],
71 | ["season": "2008", "driver": "Lewis Hamilton", "age": "23", "team": "McLaren", "engine": "Mercedes", "poles": "7", "wins": "5", "podiums": "10", "fastest_laps": "1", "points": "98", "clinched": "Race 18 of 18", "point_margin": "1"],
72 | ["season": "2009", "driver": "Jenson Button", "age": "29", "team": "Brawn", "engine": "Mercedes", "poles": "4", "wins": "6", "podiums": "9", "fastest_laps": "2", "points": "95", "clinched": "Race 16 of 17", "point_margin": "11"],
73 | ["season": "2010", "driver": "Sebastian Vettel", "age": "23", "team": "Red Bull", "engine": "Renault", "poles": "10", "wins": "5", "podiums": "10", "fastest_laps": "3", "points": "256", "clinched": "Race 19 of 19", "point_margin": "4"],
74 | ["season": "2011", "driver": "Sebastian Vettel", "age": "24", "team": "Red Bull", "engine": "Renault", "poles": "15", "wins": "11", "podiums": "17", "fastest_laps": "3", "points": "392", "clinched": "Race 15 of 19", "point_margin": "122"],
75 | ["season": "2012", "driver": "Sebastian Vettel", "age": "25", "team": "Red Bull", "engine": "Renault", "poles": "6", "wins": "5", "podiums": "10", "fastest_laps": "6", "points": "281", "clinched": "Race 20 of 20", "point_margin": "3"],
76 | ["season": "2013", "driver": "Sebastian Vettel", "age": "26", "team": "Red Bull", "engine": "Renault", "poles": "9", "wins": "13", "podiums": "16", "fastest_laps": "7", "points": "397", "clinched": "Race 16 of 19", "point_margin": "155"],
77 | ["season": "2014", "driver": "Lewis Hamilton", "age": "29", "team": "Mercedes", "engine": "Mercedes", "poles": "7", "wins": "11", "podiums": "16", "fastest_laps": "7", "points": "384", "clinched": "Race 19 of 19", "point_margin": "67"]
78 | ]
79 | static let columnsTitles = ["Year", "Driver", "Age", "Team", "Engine", "Poles", "Wins", "Podiums", "Fastest\nlaps", "Points", "Clinched", "Points\nmargin"]
80 | static let columns = ["season", "driver", "age", "team", "engine", "poles", "wins", "podiums", "fastest_laps", "points", "clinched", "point_margin"]
81 | static let columnsWidths: [CGFloat] = [70, 150, 60, 120, 110, 70, 70, 95, 70, 75, 130, 70]
82 | }
83 |
--------------------------------------------------------------------------------
/Example/GlyuckDataGrid/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 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
--------------------------------------------------------------------------------
/Source/DataGridView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataGridView.swift
3 | // Pods
4 | //
5 | // Created by Vladimir Lyukov on 30/07/15.
6 | //
7 | //
8 |
9 | import UIKit
10 |
11 |
12 | /**
13 | An object that adopts the DataGridViewDataSource protocol is responsible for providing the data and views required by a collection view. Just like UITableViewDataSource or UICollectionView data source.
14 |
15 | At a minimum, all data source objects must implement the numberOfColumnsInDataGridView:, numberOfRowsInDataGridView, dataGridView:titleForHeaderForColumn: and either dataGridView:cellForItemAtIndexPath: or dataGridView:textForCellAtIndexPath: methods. These methods are responsible for returning the number of rows/columns in the data grid view along with the cells.
16 | */
17 | @objc public protocol DataGridViewDataSource {
18 | /**
19 | Asks the data source object for number of columns in specified data grid view.
20 |
21 | - parameter dataGridView: The data grid view requesting information.
22 |
23 | - returns: Number of columns in data grid view.
24 | */
25 | func numberOfColumnsInDataGridView(_ dataGridView: DataGridView) -> Int
26 |
27 | /**
28 | Asks the data source object for number of rows in specified data grid view.
29 |
30 | - parameter dataGridView: The data grid view requesting information.
31 |
32 | - returns: Number of rows in data grid view.
33 | */
34 | func numberOfRowsInDataGridView(_ dataGridView: DataGridView) -> Int
35 |
36 | /**
37 | Asks the data source for title of header of the specified column in data grid view. You should use either this method or dataGridView:viewForHeaderForColumn: to configure header view.
38 |
39 | - parameter dataGridView: The data grid view requesting information.
40 | - parameter column: An index number identifying column of data grid view.
41 |
42 | - returns: A string to use as title for column header.
43 | */
44 | @objc optional func dataGridView(_ dataGridView: DataGridView, titleForHeaderForColumn column: Int) -> String
45 |
46 | /**
47 | Asks the data source for a view object to display in the header of the specified column of the data grid view. If implemented, dataGridView:titleForHeaderForColumn: is not called.
48 |
49 | - parameter dataGridView: The data grid view requesting information.
50 | - parameter column: An index number identifying column of data grid view.
51 |
52 | - returns: A view object to be displayed in the header of the column.
53 | */
54 | @objc optional func dataGridView(_ dataGridView: DataGridView, viewForHeaderForColumn column: Int) -> DataGridViewColumnHeaderCell
55 |
56 |
57 | /**
58 | Asks the data source for title of header of the specified row in data grid view. You should use either this method or dataGridView:viewForHeaderForRow: to configure header view.
59 |
60 | - parameter dataGridView: The data grid view requesting information.
61 | - parameter row: An index number identifying row of data grid view.
62 |
63 | - returns: A string to use as title for row header.
64 | */
65 | @objc optional func dataGridView(_ dataGridView: DataGridView, titleForHeaderForRow row: Int) -> String
66 |
67 | /**
68 | Asks the data source for a view object to display in the header of the specified row of the data grid view. If implemented, dataGridView:titleForHeaderForRow: is not called.
69 |
70 | - parameter dataGridView: The data grid view requesting information.
71 | - parameter column: An index number identifying row of data grid view.
72 |
73 | - returns: A view object to be displayed in the header of the row.
74 | */
75 | @objc optional func dataGridView(_ dataGridView: DataGridView, viewForHeaderForRow row: Int) -> DataGridViewRowHeaderCell
76 |
77 | /**
78 | Asks the data source for a cell to insert in a particular location of the data grid view.
79 |
80 | You should use either this method or dataGridView:textForCellAtIndexPath: to configure your cells.
81 |
82 | - parameter dataGridView: The data grid view requesting information.
83 | - parameter indexPath: An index path locating cell in data grid view. Be sure to use .dataGridColumn and .dataGridRow properties.
84 |
85 | - returns: An object inheriting from UICollectionViewCell that the data grid view can use for the specified row+column.
86 | */
87 | @objc optional func dataGridView(_ dataGridView: DataGridView, cellForItemAtIndexPath indexPath: IndexPath) -> UICollectionViewCell
88 |
89 | /**
90 | Asks the data source for text to be placed in default cell.
91 |
92 | You should use either this method or dataGridView:cellForItemAtIndexPath:
93 |
94 | - parameter dataGridView: The data grid view requesting information.
95 | - parameter indexPath: An index path locating cell in data grid view. Be sure to use .dataGridColumn and .dataGridRow properties.
96 |
97 | - returns: A string to us as text for default cell.
98 | */
99 | @objc optional func dataGridView(_ dataGridView: DataGridView, textForCellAtIndexPath indexPath: IndexPath) -> String
100 | }
101 |
102 |
103 | /**
104 | The DataGridViewDelegate protocol defines methods that allow you to manage the selection and highlighting of cells in a data grid view and to perform actions on those cells. The methods of this protocol are all optional.
105 |
106 | Many methods of this protocol take NSIndexPath objects as parameters. Be sure to use .dataGridColumn and .dataGridRow properties. Multiple sections are not supported.
107 |
108 | */
109 | @objc public protocol DataGridViewDelegate {
110 | /**
111 | Asks the delegate for the width to use for the specified column of data grid view.
112 |
113 | - parameter dataGridView: The data grid view requesting information.
114 | - parameter column: An index number identifying column of data grid view.
115 |
116 | - returns: A nonnegative floating-point value that specifies the width (in points) of the specified column for data grid view.
117 | */
118 | @objc optional func dataGridView(_ dataGridView: DataGridView, widthForColumn column: Int) -> CGFloat
119 |
120 | /**
121 | Asks the delegate for the height to use for the specified row.
122 |
123 | - parameter dataGridView: The data grid view requesting information.
124 | - parameter row: An index number identifying row of data grid view.
125 |
126 | - returns: A nonnegative floating-point value that specifies the height (in points) of the specified row for data grid view.
127 | */
128 | @objc optional func dataGridView(_ dataGridView: DataGridView, heightForRow row: Int) -> CGFloat
129 |
130 | /**
131 | Asks the delegate if specified column should be always kept visible when user scrolls data grid view horizontally.
132 |
133 | - parameter dataGridView: The data grid view requesting information.
134 | - parameter column: An index number identifying column of data grid view.
135 |
136 | - returns: true if the row should float or false if it should not.
137 | */
138 | @objc optional func dataGridView(_ dataGridView: DataGridView, shouldFloatColumn column: Int) -> Bool
139 |
140 | /**
141 | Asks the delegate if it accepts sorting by the specified column of data grid view.
142 |
143 | - parameter dataGridView: The data grid view requesting information.
144 | - parameter column: An index number identifying column of data grid view.
145 |
146 | - returns: true if the data grid can be sorted by specified column or false if it can not.
147 | */
148 | @objc optional func dataGridView(_ dataGridView: DataGridView, shouldSortByColumn column: Int) -> Bool
149 |
150 | /**
151 | Tells the delegate that user updated sorting column/order of data ggrid view.
152 |
153 | - parameter dataGridView: The data grid view object that is notifying you of the sorting change.
154 | - parameter column: An index number identifying new sort column of data grid view.
155 | - parameter ascending: Boolean indicating sort direction. True if should sort in ascending order or false if descending order.
156 | */
157 | @objc optional func dataGridView(_ dataGridView: DataGridView, didSortByColumn column: Int, ascending: Bool)
158 |
159 | /**
160 | Asks the delegate if the specified row should be selected.
161 |
162 | - parameter dataGridView: The data grid view requesting information.
163 | - parameter row: An index number identifying row of data grid view.
164 |
165 | - returns: true if the row should be selected or false if it should not.
166 | */
167 | @objc optional func dataGridView(_ dataGridView: DataGridView, shouldSelectRow row: Int) -> Bool
168 |
169 | /**
170 | Tells the delegate that the row at the specified index was selected.
171 |
172 | - parameter dataGridView: The data grid view object that is notifying you of the selection change.
173 | - parameter row: An index number identifying row that was selected.
174 | */
175 | @objc optional func dataGridView(_ dataGridView: DataGridView, didSelectRow row: Int)
176 | }
177 |
178 |
179 | /**
180 | An instance of DataGridView (or simply, a data grid view) is a means for displaying and editing data represented in multicolumn tables (or 2-dimension matrices).
181 | */
182 | open class DataGridView: UIView {
183 | private static var __once: () = {
184 | let appearance = DataGridView.appearance()
185 | appearance.row1BackgroundColor = UIColor(white: 0.95, alpha: 1)
186 | appearance.row2BackgroundColor = UIColor.white
187 | }()
188 | /// Constants for reuse identifiers for default cells.
189 | public enum ReuseIdentifiers {
190 | public static let defaultColumnHeader = "DataGridViewColumnHeaderCell"
191 | public static let defaultRowHeader = "DataGridViewRowHeaderCell"
192 | public static let defaultCornerHeader = "DataGridViewRowHeaderCell"
193 | public static let defaultCell = "DataGridViewContentCell"
194 | }
195 |
196 | /// Constants for supplementary view kinds of internally-used collection view.
197 | public enum SupplementaryViewKind: String {
198 | /// Header displayed on top of each column.
199 | case ColumnHeader = "ColumnHeader"
200 | /// Header displayed on left of each row.
201 | case RowHeader = "RowHeader"
202 | /// One header positioned on top-left corner of data grid view. Only displayed if data grid view has both column and row headers.
203 | case CornerHeader = "CornerHeader"
204 | }
205 |
206 | /// Collection view used internally to build up data grid.
207 | fileprivate(set) open lazy var collectionView: UICollectionView = {
208 | let layout = DataGridViewLayout(dataGridView: self)
209 | let collectionView = UICollectionView(frame: self.bounds, collectionViewLayout: layout)
210 | collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
211 | collectionView.backgroundColor = UIColor.clear
212 | collectionView.allowsMultipleSelection = true
213 | collectionView.dataSource = self.collectionViewDataSource
214 | collectionView.delegate = self.collectionViewDelegate
215 | self.addSubview(collectionView)
216 | return collectionView
217 | }()
218 |
219 | /// Object incapsulates logic for data source of collection view.
220 | open lazy var collectionViewDataSource: CollectionViewDataSource = {
221 | return CollectionViewDataSource(dataGridView: self)
222 | }()
223 |
224 | /// Object incapsulates logic for data source of collection view.
225 | open lazy var collectionViewDelegate: CollectionViewDelegate = {
226 | return CollectionViewDelegate(dataGridView: self)
227 | }()
228 |
229 | /// The object that provides the data for the data grid view.
230 | open weak var dataSource: DataGridViewDataSource?
231 | /// The object that acts as the delegate of the data grid view.
232 | open weak var delegate: DataGridViewDelegate?
233 |
234 | /// Height for every row in data grid view
235 | open var rowHeight: CGFloat = 44
236 | /// Height for header row
237 | open var columnHeaderHeight: CGFloat = 44
238 | /// Width for vertical header displayed on left of each row. If zero, vertical headers are not displayed.
239 | open var rowHeaderWidth: CGFloat = 0
240 | /// Background color for even rows of zebra-striped tables.
241 | open dynamic var row1BackgroundColor: UIColor?
242 | /// Background color for odd rows of zebra-striped tables.
243 | open dynamic var row2BackgroundColor: UIColor?
244 |
245 | /// Current sort column of data grid view.
246 | fileprivate(set) open var sortColumn: Int?
247 | /// Current sort order of data grid view.
248 | fileprivate(set) open var sortAscending = true
249 |
250 | /**
251 | Tells data grid view to sort data by specified column in specified order. Will update UI for specified column header (add an arrow indicating sort direction).
252 |
253 | - parameter column: An index number identifying column of data grid view.
254 | - parameter ascending: Boolean indicating sort direction. True if should sort in ascending order or false if descending order.
255 | */
256 | open func setSortColumn(_ column: Int, ascending: Bool) {
257 | sortColumn = column
258 | sortAscending = ascending
259 | delegate?.dataGridView?(self, didSortByColumn: column, ascending: ascending)
260 | reloadData()
261 | }
262 |
263 | /**
264 | Returns number of columns in data grid view.
265 |
266 | - returns: The number of columns in data grid view.
267 | */
268 | open func numberOfColumns() -> Int {
269 | return dataSource?.numberOfColumnsInDataGridView(self) ?? 0
270 | }
271 |
272 | /**
273 | Returns number of rows in data grid view.
274 |
275 |
276 | - returns: The number of rows in data grid view.
277 | */
278 | open func numberOfRows() -> Int {
279 | return dataSource?.numberOfRowsInDataGridView(self) ?? 0
280 | }
281 |
282 | /**
283 | This function is used to configure data grid view after creation. Register default cells, setup colors, etc.
284 | */
285 | open func setupDataGridView() {
286 | registerClass(DataGridViewContentCell.self, forCellWithReuseIdentifier: ReuseIdentifiers.defaultCell)
287 | registerClass(DataGridViewColumnHeaderCell.self, forHeaderOfKind: .ColumnHeader, withReuseIdentifier: ReuseIdentifiers.defaultColumnHeader)
288 | registerClass(DataGridViewRowHeaderCell.self, forHeaderOfKind: .RowHeader, withReuseIdentifier: ReuseIdentifiers.defaultRowHeader)
289 | registerClass(DataGridViewCornerHeaderCell.self, forHeaderOfKind: .CornerHeader, withReuseIdentifier: ReuseIdentifiers.defaultCornerHeader)
290 | }
291 |
292 | /**
293 | Reloads the rows and columns of the data grid view.
294 | */
295 | open func reloadData() {
296 | collectionView.reloadData()
297 | }
298 |
299 | /**
300 | Highlights the specified row in the data grid view. Highlights only visible cells.
301 |
302 | - parameter row: An index number identifying row to be highlighted.
303 | */
304 | open func highlightRow(_ row: Int) {
305 | for column in 0.. UICollectionViewCell {
383 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)
384 | if indexPath.dataGridRow % 2 == 0 {
385 | cell.backgroundColor = row1BackgroundColor
386 | } else {
387 | cell.backgroundColor = row2BackgroundColor
388 | }
389 | return cell
390 | }
391 |
392 | /**
393 | Registers a nib file for use in creating header views for the data grid view.
394 |
395 | - parameter nib: The nib object containing the view object. The nib file must contain only one top-level object and that object must be of the type DataGridViewRowHeaderCell.
396 | - parameter kind: Specified nib will be used to create headers of specified kind.
397 | - parameter identifier: The reuse identifier for the view. This parameter must not be nil and must not be an empty string.
398 | */
399 | open func registerNib(_ nib: UINib, forHeaderOfKind kind: SupplementaryViewKind, withReuseIdentifier identifier: String) {
400 | collectionView.register(nib, forSupplementaryViewOfKind: kind.rawValue, withReuseIdentifier: identifier)
401 | }
402 |
403 | /**
404 | Registers a class for use in creating column header views for the data grid view.
405 |
406 | - parameter cellClass: The class of a column header view that you want to use in the data grid view.
407 | - parameter kind: Specified class will be used to create headers of specified kind.
408 | - parameter identifier: The reuse identifier for the view. This parameter must not be nil and must not be an empty string.
409 | */
410 | open func registerClass(_ cellClass: DataGridViewBaseCell.Type, forHeaderOfKind kind: SupplementaryViewKind, withReuseIdentifier identifier: String) {
411 | collectionView.register(cellClass, forSupplementaryViewOfKind: kind.rawValue, withReuseIdentifier: identifier)
412 | }
413 |
414 | /**
415 | Returns a resuable view for the specified column header located by identifier.
416 |
417 | - parameter identifier: The reuse identifier for the specified column header view. This parameter must not be nil.
418 | - parameter column: An index number identifying column of data grid view.
419 |
420 | - returns: A DataGridViewColumnHeaderCell object with the associated reuse identifier. This method always returns a valid view.
421 | */
422 | open func dequeueReusableHeaderViewWithReuseIdentifier(_ identifier: String, forColumn column: NSInteger) -> DataGridViewColumnHeaderCell {
423 | let indexPath = IndexPath(index: column)
424 | let cell = collectionView.dequeueReusableSupplementaryView(ofKind: SupplementaryViewKind.ColumnHeader.rawValue, withReuseIdentifier: identifier, for: indexPath)
425 | guard let headerCell = cell as? DataGridViewColumnHeaderCell else {
426 | fatalError("Error in dequeueReusableHeaderViewWithReuseIdentifier(\(identifier), forColumn:\(column)): expected to receive object of DataGridViewColumnHeaderCell class, got \(String(describing: cell.self)) instead")
427 | }
428 | headerCell.configureForDataGridView(self, indexPath: indexPath)
429 | headerCell.isSorted = column == sortColumn
430 | headerCell.isSortedAsc = sortAscending
431 | return headerCell
432 | }
433 |
434 | /**
435 | Returns a resuable view for the specified column header located by identifier.
436 |
437 | - parameter identifier: The reuse identifier for the specified column header view. This parameter must not be nil.
438 | - parameter column: An index number identifying column of data grid view.
439 |
440 | - returns: A DataGridViewColumnHeaderCell object with the associated reuse identifier. This method always returns a valid view.
441 | */
442 | open func dequeueReusableHeaderViewWithReuseIdentifier(_ identifier: String, forRow row: NSInteger) -> DataGridViewRowHeaderCell {
443 | let indexPath = IndexPath(index: row)
444 | let cell = collectionView.dequeueReusableSupplementaryView(ofKind: SupplementaryViewKind.RowHeader.rawValue, withReuseIdentifier: identifier, for: indexPath)
445 | guard let headerCell = cell as? DataGridViewRowHeaderCell else {
446 | fatalError("Error in dequeueReusableHeaderViewWithReuseIdentifier(\(identifier), forRow:\(row)): expected to receive object of DataGridViewRowHeaderCell class, got \(String(describing: cell.self)) instead")
447 | }
448 | headerCell.configureForDataGridView(self, indexPath: indexPath)
449 | return headerCell
450 | }
451 |
452 | /**
453 | Returns a resuable view for the specified column header located by identifier.
454 |
455 | - parameter identifier: The reuse identifier for the specified column header view. This parameter must not be nil.
456 | - parameter column: An index number identifying column of data grid view.
457 |
458 | - returns: A DataGridViewColumnHeaderCell object with the associated reuse identifier. This method always returns a valid view.
459 | */
460 | open func dequeueReusableCornerHeaderViewWithReuseIdentifier(_ identifier: String) -> DataGridViewCornerHeaderCell {
461 | let indexPath = IndexPath(index: 0)
462 | let cell = collectionView.dequeueReusableSupplementaryView(ofKind: SupplementaryViewKind.CornerHeader.rawValue, withReuseIdentifier: identifier, for: indexPath)
463 | guard let headerCell = cell as? DataGridViewCornerHeaderCell else {
464 | fatalError("Error in dequeueReusableCornerHeaderViewWithReuseIdentifier(\(identifier)): expected to receive object of DataGridViewCornerHeaderCell class, got \(String(describing: cell.self)) instead")
465 | }
466 | headerCell.configureForDataGridView(self, indexPath: indexPath)
467 | return headerCell
468 | }
469 |
470 | // UIView
471 |
472 | open override static func initialize() {
473 | super.initialize()
474 | _ = DataGridView.__once
475 | }
476 |
477 | public override init(frame: CGRect) {
478 | super.init(frame: frame)
479 | setupDataGridView()
480 | }
481 |
482 | public required init?(coder aDecoder: NSCoder) {
483 | super.init(coder: aDecoder)
484 | setupDataGridView()
485 | }
486 |
487 | // UIScrollView
488 |
489 | open var contentOffset: CGPoint {
490 | set { collectionView.contentOffset = newValue }
491 | get { return collectionView.contentOffset }
492 | }
493 |
494 | open func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
495 | collectionView.setContentOffset(contentOffset, animated: animated)
496 | }
497 | }
498 |
--------------------------------------------------------------------------------