├── 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 | [![CI Status](http://img.shields.io/travis/glyuck/GlyuckDataGrid.svg?style=flat)](https://travis-ci.org/Glyuck/GlyuckDataGrid) 3 | [![Version](https://img.shields.io/cocoapods/v/GlyuckDataGrid.svg?style=flat)](http://cocoapods.org/pods/GlyuckDataGrid) 4 | [![Quality](https://apps.e-sites.nl/cocoapodsquality/GlyuckDataGrid/badge.svg?clear_cache)](https://cocoapods.org/pods/GlyuckDataGrid/quality) 5 | [![License](https://img.shields.io/cocoapods/l/GlyuckDataGrid.svg?style=flat)](http://cocoapods.org/pods/GlyuckDataGrid) 6 | [![Platform](https://img.shields.io/cocoapods/p/GlyuckDataGrid.svg?style=flat)](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 | ![Screenshot](https://raw.githubusercontent.com/glyuck/GlyuckDataGrid/master/Example/screenshot_01.png) ![Screenshot](https://raw.githubusercontent.com/glyuck/GlyuckDataGrid/master/Example/screenshot_02.png) 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 | --------------------------------------------------------------------------------