├── .gitignore ├── .travis.yml ├── G3GridView.podspec ├── GridView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── GridView.xcscheme │ └── GridViewTests.xcscheme ├── GridView ├── AnimatedLayer.swift ├── AroundInsets.swift ├── ArrayExtension.swift ├── CGFloatExtension.swift ├── CGRectExtension.swift ├── CGSizeExtension.swift ├── Countable.swift ├── GridView+DelegateProxy.m ├── GridView.h ├── GridView.swift ├── GridViewCell.swift ├── GridViewScrollPosition.swift ├── Horizontal.swift ├── IndexPathExtension.swift ├── Info.plist ├── Location.swift ├── NSObjectProtocolExtension.swift ├── NeedsLayout.swift ├── ReuseQueue.swift ├── Scale.swift ├── UIEdgeInsetsExtension.swift ├── UIScrollViewExtension.swift ├── Vertical.swift ├── ViewBundle.swift ├── ViewMatrix.swift ├── ViewReference.swift └── ViewVisibleInfo.swift ├── GridViewExample ├── GridViewExample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── GridViewExample │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── ChannelListGridViewCell.swift │ ├── ChannelListGridViewCell.xib │ ├── DateTimeGridViewCell.swift │ ├── DateTimeGridViewCell.xib │ ├── Info.plist │ ├── PageGridViewCell.swift │ ├── PageGridViewCell.xib │ ├── PageViewController.swift │ ├── Slot.swift │ ├── TimeTableGridViewCell.swift │ ├── TimeTableGridViewCell.xib │ └── TimeTableViewController.swift ├── GridViewTests ├── AroundInsetsTests.swift ├── ArrayExtensionTests.swift ├── CGFloatExtensionTests.swift ├── CGRectExtensionTests.swift ├── CGSizeExtensionTests.swift ├── CountableTests.swift ├── GridViewCellTests.swift ├── GridViewScrollPositionTests.swift ├── GridViewTests.swift ├── HorizontalTests.swift ├── IndexPathTests.swift ├── Info.plist ├── LocationTests.swift ├── Mock │ ├── MockCell.swift │ ├── MockCell.xib │ ├── MockLocation.swift │ └── MockReusable.swift ├── NeedsLayoutTests.swift ├── ReuseQueueTests.swift ├── ScaleTests.swift ├── UIEdgeInsetsExtensionTests.swift ├── UIScrollViewExtensionTests.swift ├── VerticalTests.swift ├── ViewBundleTests.swift ├── ViewMatrixTests.swift ├── ViewReferenceTests.swift └── ViewVisibleInfoTests.swift ├── LICENSE ├── Package.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | 20 | # CocoaPods 21 | # 22 | # We recommend against adding the Pods directory to your .gitignore. However 23 | # you should judge for yourself, the pros and cons are mentioned at: 24 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 25 | # 26 | # Pods/ 27 | 28 | # Carthage 29 | # 30 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 31 | # Carthage/Checkouts 32 | 33 | Carthage/Build 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | os: osx 3 | osx_image: xcode10.3 4 | before_install: 5 | - gem install xcpretty 6 | before_script: 7 | - set -o pipefail 8 | script: 9 | - xcodebuild test -project ./GridView.xcodeproj -scheme GridViewTests -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=12.4,name=iPhone 8' | xcpretty -c 10 | notifications: 11 | email: false 12 | -------------------------------------------------------------------------------- /G3GridView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "G3GridView" 3 | s.summary = "Reusable GridView with excellent performance and customization that can be time table, spreadsheet, paging and more." 4 | s.homepage = "https://github.com/KyoheiG3/GridView" 5 | s.version = "0.8.0" 6 | s.license = { :type => "MIT", :file => "LICENSE" } 7 | s.author = { "Kyohei Ito" => "je.suis.kyohei@gmail.com" } 8 | s.swift_version = '5.0' 9 | s.ios.deployment_target = '9.0' 10 | s.source = { :git => "https://github.com/KyoheiG3/GridView.git", :tag => s.version.to_s } 11 | s.source_files = "GridView/**/*.{h,m,swift}" 12 | s.module_name = "GridView" 13 | s.requires_arc = true 14 | s.frameworks = "UIKit" 15 | end 16 | -------------------------------------------------------------------------------- /GridView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /GridView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /GridView.xcodeproj/xcshareddata/xcschemes/GridView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /GridView.xcodeproj/xcshareddata/xcschemes/GridViewTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 40 | 41 | 42 | 43 | 49 | 50 | 52 | 53 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /GridView/AnimatedLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatedLayer.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/11/08. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol AnimatedLayerDelegate: CALayerDelegate { 12 | func animatedLayer(_ layer: AnimatedLayer, statusDidChange status: AnimatedLayer.Status) 13 | } 14 | 15 | extension AnimatedLayerDelegate { 16 | func animatedLayer(_ layer: AnimatedLayer, statusDidChange status: AnimatedLayer.Status) { 17 | // Do nothing 18 | } 19 | } 20 | 21 | class AnimatedLayer: CALayer { 22 | enum Status { 23 | case cancelled, finished 24 | } 25 | 26 | private static let animationkey = "progress" 27 | fileprivate struct Animation { 28 | let key = AnimatedLayer.animationkey 29 | let from: CGFloat = 0 30 | let to: CGFloat = 1 31 | } 32 | 33 | override class func needsDisplay(forKey key: String) -> Bool { 34 | if key == animationkey { 35 | return true 36 | } 37 | return super.needsDisplay(forKey: key) 38 | } 39 | 40 | private var isAnimating = false 41 | private var isAnimatingFinish: Bool { 42 | if isAnimating, let layer = presentation() { 43 | return layer.progress == animation.to 44 | } else { 45 | return false 46 | } 47 | } 48 | fileprivate var isProgressing: Bool { 49 | return isAnimating == true && isAnimatingFinish == false 50 | } 51 | fileprivate let animation = Animation() 52 | 53 | override func action(forKey event: String) -> CAAction? { 54 | if event == animation.key { 55 | isAnimating = false 56 | if let action = super.action(forKey: "opacity") as? CABasicAnimation { 57 | isAnimating = true 58 | 59 | action.keyPath = event 60 | action.fromValue = animation.from 61 | action.toValue = animation.to 62 | return action 63 | } 64 | } 65 | 66 | return super.action(forKey: event) 67 | } 68 | 69 | override func display() { 70 | super.display() 71 | 72 | if isProgressing == false { 73 | setCurrentProgress(animation.from, status: .finished) 74 | } 75 | } 76 | } 77 | 78 | extension AnimatedLayer { 79 | @NSManaged private(set) fileprivate var progress: CGFloat 80 | private var animatedDelegate: AnimatedLayerDelegate? { 81 | return delegate as? AnimatedLayerDelegate 82 | } 83 | 84 | fileprivate func setCurrentProgress(_ progress: CGFloat, status: Status? = nil) { 85 | if let status = status { 86 | animatedDelegate?.animatedLayer(self, statusDidChange: status) 87 | } 88 | 89 | if delegate != nil { 90 | self.progress = progress 91 | } 92 | } 93 | } 94 | 95 | extension AnimatedLayer { 96 | func animate() { 97 | if isProgressing { 98 | UIView.performWithoutAnimation { 99 | setCurrentProgress(animation.from, status: .cancelled) 100 | } 101 | } 102 | 103 | setCurrentProgress(animation.to) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /GridView/AroundInsets.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AroundInsets.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/11/25. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct AroundInsets { 12 | struct Inset { 13 | static let zero = Inset(width: 0) 14 | 15 | let width: CGFloat 16 | 17 | init(width: CGFloat) { 18 | self.width = width 19 | } 20 | } 21 | 22 | static let zero = AroundInsets(parentSize: .zero, frame: .zero) 23 | var left: Inset 24 | var right: Inset 25 | 26 | init(parentSize: CGSize, frame: CGRect) { 27 | func around(_ x: CGFloat) -> CGFloat { 28 | return ceil(x / frame.width) * frame.width 29 | } 30 | 31 | if frame.width > 0 { 32 | left = Inset(width: around(frame.minX)) 33 | right = Inset(width: around(parentSize.width - frame.maxX)) 34 | } else { 35 | left = .zero 36 | right = .zero 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /GridView/ArrayExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayExtension.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/11/03. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Array where Element: Hashable, Element: Comparable { 12 | func union(_ elements: [Element]) -> [Element] { 13 | return self + elements.subtracting(self) 14 | } 15 | 16 | func subtracting(_ elements: [Element]) -> [Element] { 17 | return [Element](Set(self).subtracting(elements)).sorted() 18 | } 19 | 20 | func intersection(_ elements: [Element]) -> [Element] { 21 | return [Element](Set(self).intersection(elements)).sorted() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /GridView/CGFloatExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGFloatExtension.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2017/01/02. 6 | // Copyright © 2017年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | 11 | extension CGFloat { 12 | fileprivate static var scale = UIScreen.main.scale 13 | var integral: CGFloat { 14 | return CoreGraphics.round(self * CGFloat.scale) / CGFloat.scale 15 | } 16 | 17 | func rounded(p: CGFloat) -> CGFloat { 18 | let scale = pow(10, p) 19 | return CoreGraphics.round(self * scale) / scale 20 | } 21 | 22 | func floored(p: CGFloat) -> CGFloat { 23 | let scale = pow(10, p) 24 | return CoreGraphics.floor(self * scale) / scale 25 | } 26 | } 27 | 28 | #if DEBUG 29 | extension CGFloat { 30 | static var debugScale: CGFloat { 31 | get { return scale } 32 | set { scale = newValue } 33 | } 34 | } 35 | #endif 36 | -------------------------------------------------------------------------------- /GridView/CGRectExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGRectExtension.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/11/26. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | 11 | extension CGRect { 12 | init(horizontal: Horizontal) { 13 | self.init(x: horizontal.x, y: 0, width: horizontal.width, height: 0) 14 | } 15 | init(vertical: Vertical) { 16 | self.init(x: 0, y: vertical.y, width: 0, height: vertical.height) 17 | } 18 | init(horizontal: Horizontal, vertical: Vertical) { 19 | self.init(x: horizontal.x, y: vertical.y, width: horizontal.width, height: vertical.height) 20 | } 21 | 22 | var vertical: Vertical { 23 | get { 24 | return Vertical(y: origin.y, height: height) 25 | } 26 | set { 27 | origin.y = newValue.y 28 | size.height = newValue.height 29 | } 30 | } 31 | 32 | var horizontal: Horizontal { 33 | get { 34 | return Horizontal(x: origin.x, width: width) 35 | } 36 | set { 37 | origin.x = newValue.x 38 | size.width = newValue.width 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /GridView/CGSizeExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGSizeExtension.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2017/01/29. 6 | // Copyright © 2017年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | 11 | extension CGSize { 12 | static func +(lhs: CGSize, rhs: CGSize) -> CGSize { 13 | return CGSize(width: lhs.width + rhs.width, height: lhs.height + rhs.height) 14 | } 15 | 16 | static func -(lhs: CGSize, rhs: CGSize) -> CGSize { 17 | return CGSize(width: lhs.width - rhs.width, height: lhs.height - rhs.height) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /GridView/Countable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Countable.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/11/23. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | enum Threshold { 10 | case `in`, above, below 11 | } 12 | 13 | protocol Countable { 14 | var count: Int { get } 15 | 16 | func `repeat`(_ value: Int) -> Int 17 | func threshold(with value: Int) -> Threshold 18 | } 19 | 20 | extension Dictionary: Countable {} 21 | extension Array: Countable {} 22 | 23 | extension Countable { 24 | func `repeat`(_ x: Int) -> Int { 25 | guard count != 0 && (x < 0 || x >= count) else { 26 | return x 27 | } 28 | 29 | var value = x 30 | while value < 0 { 31 | value += count 32 | } 33 | return value % count 34 | } 35 | 36 | func threshold(with value: Int) -> Threshold { 37 | switch value { 38 | case (let s) where s < 0: 39 | return .below 40 | case (let s) where s >= count: 41 | return .above 42 | default: 43 | return .in 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /GridView/GridView+DelegateProxy.m: -------------------------------------------------------------------------------- 1 | // 2 | // GridView+DelegateProxy.m 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2017/01/20. 6 | // Copyright © 2017年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @implementation GridView (DelegateProxy) 13 | 14 | - (void)forwardInvocation:(NSInvocation *)invocation { 15 | [invocation invokeWithTarget: self.originDelegate]; 16 | } 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /GridView/GridView.h: -------------------------------------------------------------------------------- 1 | // 2 | // GridView.h 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/11/27. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for GridView. 12 | FOUNDATION_EXPORT double GridViewVersionNumber; 13 | 14 | //! Project version string for GridView. 15 | FOUNDATION_EXPORT const unsigned char GridViewVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /GridView/GridViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridViewCell.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/10/30. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class GridViewCell: UIView, Reusable { 12 | open internal(set) var indexPath = IndexPath(row: 0, column: 0) 13 | open var isSelected: Bool = false { 14 | didSet { 15 | setSelected(isSelected) 16 | } 17 | } 18 | 19 | open var isHighlighted: Bool = false { 20 | didSet { 21 | setHighlighted(isHighlighted) 22 | } 23 | } 24 | 25 | open func prepareForReuse() { 26 | } 27 | 28 | open func setSelected(_ selected: Bool) { 29 | } 30 | 31 | open func setHighlighted(_ highlighted: Bool) { 32 | } 33 | 34 | public required init?(coder aDecoder: NSCoder) { 35 | super.init(coder: aDecoder) 36 | autoresizingMask = .init(rawValue: 0) 37 | } 38 | 39 | public required override init(frame: CGRect) { 40 | super.init(frame: frame) 41 | autoresizingMask = .init(rawValue: 0) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /GridView/GridViewScrollPosition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridViewScrollPosition.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/11/28. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | public struct GridViewScrollPosition: OptionSet { 10 | public private(set) var rawValue: UInt 11 | 12 | public init(rawValue: UInt) { 13 | self.rawValue = rawValue 14 | } 15 | 16 | // The vertical positions are mutually exclusive to each other, but are bitwise or-able with the horizontal scroll positions. 17 | // Combining positions from the same grouping (horizontal or vertical) will result in an NSInvalidArgumentException. 18 | public static var top = GridViewScrollPosition(rawValue: 1 << 0) 19 | public static var centeredVertically = GridViewScrollPosition(rawValue: 1 << 1) 20 | public static var bottom = GridViewScrollPosition(rawValue: 1 << 2) 21 | 22 | // Likewise, the horizontal positions are mutually exclusive to each other. 23 | public static var left = GridViewScrollPosition(rawValue: 1 << 3) 24 | public static var centeredHorizontally = GridViewScrollPosition(rawValue: 1 << 4) 25 | public static var right = GridViewScrollPosition(rawValue: 1 << 5) 26 | 27 | public static var topFit = GridViewScrollPosition(rawValue: 1 << 6) 28 | public static var bottomFit = GridViewScrollPosition(rawValue: 1 << 7) 29 | public static var leftFit = GridViewScrollPosition(rawValue: 1 << 8) 30 | public static var rightFit = GridViewScrollPosition(rawValue: 1 << 9) 31 | } 32 | -------------------------------------------------------------------------------- /GridView/Horizontal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Horizontal.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/28. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | 11 | struct Horizontal: Equatable { 12 | static let zero = Horizontal(x: 0, width: 0) 13 | 14 | static func ==(lhs: Horizontal, rhs: Horizontal) -> Bool { 15 | return lhs.x == rhs.x && lhs.width == rhs.width 16 | } 17 | 18 | static func *(lhs: Horizontal, rhs: CGFloat) -> Horizontal { 19 | return Horizontal(x: lhs.x * rhs, width: lhs.width * rhs) 20 | } 21 | 22 | var x: CGFloat { 23 | didSet { 24 | maxX = maxX - oldValue + x 25 | } 26 | } 27 | var width: CGFloat { 28 | didSet { 29 | maxX = maxX - oldValue + width 30 | } 31 | } 32 | private(set) var maxX: CGFloat 33 | var integral: Horizontal { 34 | return Horizontal(origin: self) 35 | } 36 | 37 | init(x: CGFloat, width: CGFloat) { 38 | self.x = x 39 | self.width = width 40 | self.maxX = x + width 41 | } 42 | 43 | private init(origin: Horizontal) { 44 | self.x = origin.x.integral 45 | self.width = origin.width.integral 46 | self.maxX = (origin.x + origin.width).integral 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /GridView/IndexPathExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IndexPathExtension.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2017/01/21. 6 | // Copyright © 2017年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension IndexPath { 12 | public init(row: Int, column: Int) { 13 | self.init(row: row, section: column) 14 | } 15 | 16 | public var column: Int { 17 | get { return section } 18 | set { section = newValue } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /GridView/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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /GridView/Location.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Location.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/28. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol Location: Equatable { 12 | var x: CGFloat { get } 13 | var y: CGFloat { get } 14 | 15 | init(x: CGFloat, y: CGFloat) 16 | 17 | func min() -> CGFloat 18 | func max() -> CGFloat 19 | } 20 | 21 | extension Location { 22 | public static func ==(lhs: Self, rhs: Self) -> Bool { 23 | return lhs.x == rhs.x && lhs.y == rhs.y 24 | } 25 | 26 | public static func +(lhs: Self, rhs: Self) -> Self { 27 | return Self(x: lhs.x + rhs.x, y: lhs.y + rhs.y) 28 | } 29 | 30 | public static func -(lhs: Self, rhs: Self) -> Self { 31 | return Self(x: lhs.x - rhs.x, y: lhs.y - rhs.y) 32 | } 33 | 34 | public static func *(lhs: Self, rhs: Self) -> Self { 35 | return Self(x: lhs.x * rhs.x, y: lhs.y * rhs.y) 36 | } 37 | 38 | public static func /(lhs: Self, rhs: Self) -> Self { 39 | return Self(x: lhs.x / rhs.x, y: lhs.y / rhs.y) 40 | } 41 | 42 | public static func +(lhs: Self, rhs: CGFloat) -> Self { 43 | return Self(x: lhs.x + rhs, y: lhs.y + rhs) 44 | } 45 | 46 | public static func -(lhs: Self, rhs: CGFloat) -> Self { 47 | return Self(x: lhs.x - rhs, y: lhs.y - rhs) 48 | } 49 | 50 | func min() -> CGFloat { 51 | return Swift.min(x, y) 52 | } 53 | 54 | func max() -> CGFloat { 55 | return Swift.max(x, y) 56 | } 57 | } 58 | 59 | extension CGPoint: Location {} 60 | 61 | func max(_ lhs: T, _ rhs: T...) -> T { 62 | let rx = rhs.max(by: { $0.x < $1.x })?.x ?? lhs.x 63 | let ry = rhs.max(by: { $0.y < $1.y })?.y ?? lhs.y 64 | return T(x: max(lhs.x, rx), y: max(lhs.y, ry)) 65 | } 66 | 67 | func min(_ lhs: T, _ rhs: T...) -> T { 68 | let rx = rhs.min(by: { $0.x < $1.x })?.x ?? lhs.x 69 | let ry = rhs.min(by: { $0.y < $1.y })?.y ?? lhs.y 70 | return T(x: min(lhs.x, rx), y: min(lhs.y, ry)) 71 | } 72 | -------------------------------------------------------------------------------- /GridView/NSObjectProtocolExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSObjectProtocolExtension.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2017/01/29. 6 | // Copyright © 2017年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NSObjectProtocol { 12 | static var className: String { 13 | return String(describing: self) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /GridView/NeedsLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NeedsLayout.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/28. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | enum NeedsLayout: CustomDebugStringConvertible { 10 | case none, reload, layout(LayoutType) 11 | var debugDescription: String { 12 | switch self { 13 | case .none: return ".none" 14 | case .reload: return ".reload" 15 | case .layout(let type): return ".layout(\(type.debugDescription))" 16 | } 17 | } 18 | 19 | enum LayoutType: CustomDebugStringConvertible { 20 | case all(ViewMatrix), horizontally(ViewMatrix), rotating(ViewMatrix), scaling(ViewMatrix), pinching(ViewMatrix) 21 | var isScaling: Bool { 22 | switch self { 23 | case .scaling, .pinching: 24 | return true 25 | default: 26 | return false 27 | } 28 | } 29 | 30 | var debugDescription: String { 31 | switch self { 32 | case .all: return ".all" 33 | case .horizontally: return ".horizontally" 34 | case .rotating: return ".rotating" 35 | case .scaling: return ".scaling" 36 | case .pinching: return ".pinching" 37 | } 38 | } 39 | } 40 | } 41 | 42 | extension NeedsLayout: Equatable { 43 | static func ==(lhs: NeedsLayout, rhs: NeedsLayout) -> Bool { 44 | switch (lhs, rhs) { 45 | case (none, none), (reload, reload), (layout, layout): 46 | return true 47 | default: 48 | return false 49 | } 50 | } 51 | } 52 | 53 | extension NeedsLayout: Comparable { 54 | /// .none < .layout < .reload 55 | static func <(lhs: NeedsLayout, rhs: NeedsLayout) -> Bool { 56 | switch rhs { 57 | case .reload: 58 | switch lhs { 59 | case .reload: 60 | return false 61 | default: 62 | return true 63 | } 64 | case .layout: 65 | switch lhs { 66 | case .reload, .layout: 67 | return false 68 | default: 69 | return true 70 | } 71 | case .none: 72 | return false 73 | } 74 | } 75 | } 76 | 77 | extension NeedsLayout.LayoutType { 78 | var matrix: ViewMatrix { 79 | switch self { 80 | case .all(let m), .horizontally(let m), .rotating(let m), .scaling(let m), .pinching(let m): 81 | return m 82 | } 83 | } 84 | } 85 | 86 | extension NeedsLayout.LayoutType: Equatable { 87 | static func ==(lhs: NeedsLayout.LayoutType, rhs: NeedsLayout.LayoutType) -> Bool { 88 | switch (lhs, rhs) { 89 | case (all, all), (horizontally, horizontally), (rotating, rotating), (scaling, scaling), (pinching, pinching): 90 | return true 91 | default: 92 | return false 93 | } 94 | } 95 | } 96 | 97 | extension NeedsLayout.LayoutType: Comparable { 98 | /// .pinching < .rotating < .horizontally < .all 99 | static func <(lhs: NeedsLayout.LayoutType, rhs: NeedsLayout.LayoutType) -> Bool { 100 | switch rhs { 101 | case .all: 102 | switch lhs { 103 | case .all: 104 | return false 105 | default: 106 | return true 107 | } 108 | case .horizontally: 109 | switch lhs { 110 | case .all, .horizontally: 111 | return false 112 | default: 113 | return true 114 | } 115 | case .rotating: 116 | switch lhs { 117 | case .all, .horizontally, .rotating: 118 | return false 119 | default: 120 | return true 121 | } 122 | case .scaling: 123 | switch lhs { 124 | case .all, .horizontally, .rotating, scaling: 125 | return false 126 | default: 127 | return true 128 | } 129 | case .pinching: 130 | return false 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /GridView/ReuseQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReuseQueue.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/11/03. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | public protocol Reusable: class { 10 | var canReuse: Bool { get } 11 | func prepareForReuse() 12 | } 13 | 14 | extension Reusable { 15 | public func prepareForReuse() { 16 | // Do nothing 17 | } 18 | } 19 | 20 | extension Reusable where Self: UIView { 21 | public var canReuse: Bool { 22 | return superview == nil 23 | } 24 | } 25 | 26 | struct ReuseQueue { 27 | private var queue: [String: [E]] = [:] 28 | 29 | func dequeue(with identifier: String) -> E? { 30 | guard let elements = queue[identifier] else { return nil } 31 | 32 | for element in elements where element.canReuse { 33 | return element 34 | } 35 | return nil 36 | } 37 | 38 | mutating func append(_ element: E, for identifier: String) { 39 | if queue[identifier] == nil { 40 | queue[identifier] = [] 41 | } 42 | 43 | queue[identifier]?.append(element) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /GridView/Scale.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Scale.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/28. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public struct Scale: Location { 12 | public static let zero = Scale(x: 0, y: 0) 13 | 14 | public var x: CGFloat 15 | public var y: CGFloat 16 | 17 | public init(x: CGFloat, y: CGFloat) { 18 | self.x = x 19 | self.y = y 20 | } 21 | } 22 | 23 | public extension Scale { 24 | static let `default` = Scale(x: 1, y: 1) 25 | } 26 | -------------------------------------------------------------------------------- /GridView/UIEdgeInsetsExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIEdgeInsetsExtension.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2017/01/28. 6 | // Copyright © 2017年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | extension UIEdgeInsets { 10 | static func +(lhs: UIEdgeInsets, rhs: UIEdgeInsets) -> UIEdgeInsets { 11 | return UIEdgeInsets(top: lhs.top + rhs.top, left: lhs.left + rhs.left, bottom: lhs.bottom + rhs.bottom, right: lhs.right + rhs.right) 12 | } 13 | 14 | static func -(lhs: UIEdgeInsets, rhs: UIEdgeInsets) -> UIEdgeInsets { 15 | return UIEdgeInsets(top: lhs.top - rhs.top, left: lhs.left - rhs.left, bottom: lhs.bottom - rhs.bottom, right: lhs.right - rhs.right) 16 | } 17 | 18 | var horizontal: CGFloat { 19 | return left + right 20 | } 21 | 22 | var vertical: CGFloat { 23 | return top + bottom 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /GridView/UIScrollViewExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollViewExtension.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/11/10. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIScrollView { 12 | var validityContentOffset: CGPoint { 13 | return CGPoint(x: contentOffset.x - frame.minX, y: contentOffset.y - frame.minY) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /GridView/Vertical.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Vertical.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/28. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | 11 | struct Vertical: Equatable { 12 | static let zero = Vertical(y: 0, height: 0) 13 | 14 | static func ==(lhs: Vertical, rhs: Vertical) -> Bool { 15 | return lhs.y == rhs.y && lhs.height == rhs.height 16 | } 17 | 18 | static func *(lhs: Vertical, rhs: CGFloat) -> Vertical { 19 | return Vertical(y: lhs.y * rhs, height: lhs.height * rhs) 20 | } 21 | 22 | var y: CGFloat { 23 | didSet { 24 | maxY = maxY - oldValue + y 25 | } 26 | } 27 | var height: CGFloat { 28 | didSet { 29 | maxY = maxY - oldValue + height 30 | } 31 | } 32 | private(set) var maxY: CGFloat 33 | var integral: Vertical { 34 | return Vertical(origin: self) 35 | } 36 | 37 | init(y: CGFloat, height: CGFloat) { 38 | self.y = y 39 | self.height = height 40 | self.maxY = y + height 41 | } 42 | 43 | private init(origin: Vertical) { 44 | self.y = origin.y.integral 45 | self.height = origin.height.integral 46 | self.maxY = (origin.y + origin.height).integral 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /GridView/ViewBundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewBundle.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/11/04. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol Initializable { 12 | init() 13 | } 14 | 15 | extension NSObject: Initializable {} 16 | 17 | struct ViewBundle where T: NSObjectProtocol { 18 | private var nib: [String: UINib] = [:] 19 | private var `class`: [String: T.Type] = [:] 20 | 21 | mutating func register(ofNib nib: UINib?, for identifier: String) { 22 | self.nib[identifier] = nib 23 | self.class[identifier] = nil 24 | } 25 | 26 | mutating func register(ofClass cellClass: T.Type, for identifier: String) { 27 | self.class[identifier] = cellClass 28 | self.nib[identifier] = nil 29 | } 30 | 31 | private func nibInstantiate(with identifier: String) -> T? { 32 | return self.nib[identifier]?.instantiate(withOwner: nil, options: nil).first as? T 33 | } 34 | 35 | private func classInstantiate(with identifier: String) -> T? { 36 | return self.class[identifier]?.init() 37 | } 38 | 39 | func instantiate(with identifier: String) -> T { 40 | if let cell = nibInstantiate(with: identifier) ?? classInstantiate(with: identifier) { 41 | return cell 42 | } else { 43 | fatalError("could not dequeue a view of kind: \(T.className) with identifier \(identifier) - must register a nib or a class for the identifier") 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /GridView/ViewMatrix.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewMatrix.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/11/25. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct ViewMatrix: Countable { 12 | private let isInfinitable: Bool 13 | private let horizontals: [Horizontal]? 14 | private let verticals: [[Vertical?]] 15 | private let visibleSize: CGSize? 16 | private let viewFrame: CGRect 17 | private let scale: Scale 18 | private let inset: UIEdgeInsets 19 | private let aroundInset: AroundInsets 20 | private let contentHeight: CGFloat 21 | 22 | let validityContentRect: CGRect 23 | let contentSize: CGSize 24 | let contentInset: UIEdgeInsets 25 | var count: Int { 26 | return verticals.count 27 | } 28 | 29 | func convertToActualOffset(_ offset: CGPoint) -> CGPoint { 30 | return CGPoint(x: offset.x - aroundInset.left.width, y: offset.y) 31 | } 32 | 33 | func convert(_ offset: CGPoint, from matrix: ViewMatrix) -> CGPoint { 34 | let oldContentOffset = offset + matrix.viewFrame.origin 35 | let indexPath = matrix.indexPathForRow(at: oldContentOffset) 36 | 37 | let oldRect = matrix.rectForRow(at: indexPath) 38 | let oldOffset = oldContentOffset - oldRect.origin 39 | guard oldRect.width != 0 && oldRect.height != 0 else { return .zero } 40 | 41 | let newRect = rectForRow(at: indexPath) 42 | let newOffset = CGPoint(x: newRect.width * oldOffset.x / oldRect.width, y: newRect.height * oldOffset.y / oldRect.height) 43 | 44 | let contentOffset = newRect.origin + newOffset 45 | 46 | let actualSize: CGSize = contentSize - (visibleSize ?? .zero) 47 | let maxOffset = CGPoint(x: viewFrame.origin.x + inset.right + actualSize.width, y: viewFrame.origin.y + inset.bottom + actualSize.height) 48 | let minOffset = CGPoint(x: viewFrame.origin.x - inset.left, y: viewFrame.origin.y - inset.top) 49 | return min(max(contentOffset, minOffset), maxOffset) 50 | } 51 | 52 | init() { 53 | self.init(horizontals: nil, verticals: [], viewFrame: .zero, contentHeight: 0, superviewSize: nil, scale: .zero, inset: .zero, isInfinitable: false) 54 | } 55 | 56 | init(matrix: ViewMatrix, horizontals: [Horizontal]? = nil, viewFrame: CGRect, superviewSize: CGSize?, scale: Scale, inset: UIEdgeInsets) { 57 | let height = matrix.contentHeight 58 | self.init(horizontals: horizontals ?? matrix.horizontals, verticals: matrix.verticals, viewFrame: viewFrame, contentHeight: height, superviewSize: superviewSize, scale: scale, inset: inset, isInfinitable: matrix.isInfinitable) 59 | } 60 | 61 | init(horizontals: [Horizontal]?, verticals: [[Vertical?]], viewFrame: CGRect, contentHeight: CGFloat, superviewSize: CGSize?, scale: Scale, inset: UIEdgeInsets, isInfinitable: Bool) { 62 | var contentSize: CGSize = .zero 63 | contentSize.width = (horizontals?.last?.maxX ?? viewFrame.width * CGFloat(verticals.endIndex)) * scale.x 64 | if contentHeight == 0 { 65 | contentSize.height = viewFrame.height * CGFloat(verticals.first?.endIndex ?? 0) * scale.y 66 | } else { 67 | contentSize.height = contentHeight * scale.y 68 | } 69 | 70 | self.horizontals = horizontals 71 | self.verticals = verticals 72 | self.viewFrame = viewFrame 73 | self.visibleSize = superviewSize 74 | self.scale = scale 75 | self.inset = inset 76 | self.contentHeight = contentHeight 77 | self.isInfinitable = isInfinitable 78 | 79 | let parentSize = superviewSize ?? .zero 80 | if isInfinitable { 81 | let aroundInset = AroundInsets(parentSize: parentSize, frame: viewFrame) 82 | self.aroundInset = aroundInset 83 | 84 | let allWidth = aroundInset.left.width + aroundInset.right.width + viewFrame.width 85 | self.validityContentRect = CGRect(origin: CGPoint(x: aroundInset.left.width - viewFrame.minX, y: 0), size: contentSize) 86 | self.contentSize = CGSize(width: contentSize.width + allWidth, height: contentSize.height) 87 | self.contentInset = UIEdgeInsets(top: -viewFrame.minY + inset.top, left: -aroundInset.left.width, bottom: -parentSize.height + viewFrame.maxY + inset.bottom, right: -aroundInset.right.width) 88 | } else { 89 | self.aroundInset = .zero 90 | self.validityContentRect = CGRect(origin: .zero, size: contentSize) 91 | self.contentSize = contentSize 92 | self.contentInset = UIEdgeInsets(top: -viewFrame.minY, left: -viewFrame.minX, bottom: -parentSize.height + viewFrame.maxY, right: -parentSize.width + viewFrame.maxX) + inset 93 | } 94 | } 95 | 96 | fileprivate func verticalsForColumn(_ column: Int) -> [Vertical?] { 97 | guard verticals.indices ~= column else { 98 | return [] 99 | } 100 | return verticals[column] 101 | } 102 | 103 | fileprivate func vertical(of verticals: [Vertical?], at index: Int) -> Vertical { 104 | guard verticals.indices ~= index else { 105 | return .zero 106 | } 107 | guard let vertical = verticals[index] else { 108 | let viewHeight = viewFrame.height 109 | return Vertical(y: viewHeight * CGFloat(index), height: viewHeight) 110 | } 111 | return vertical 112 | } 113 | 114 | fileprivate func verticalForRow(at indexPath: IndexPath) -> Vertical { 115 | return vertical(of: verticalsForColumn(indexPath.column), at: indexPath.row) * scale.y 116 | } 117 | 118 | fileprivate func offsetXForColumn(_ column: Int) -> CGFloat { 119 | guard let horizontals = horizontals else { 120 | return 0 121 | } 122 | 123 | switch horizontals.threshold(with: column) { 124 | case .below: 125 | return -validityContentRect.width 126 | case .above: 127 | return validityContentRect.width 128 | case .in: 129 | return 0 130 | } 131 | } 132 | 133 | fileprivate func horizontalForColumn(_ column: Int) -> Horizontal { 134 | var horizontal: Horizontal 135 | if let horizontals = horizontals { 136 | let absColumn = self.repeat(column) 137 | horizontal = horizontals[absColumn] * scale.x 138 | horizontal.x += offsetXForColumn(column) 139 | } else { 140 | let viewWidth = viewFrame.width * scale.x 141 | horizontal = Horizontal(x: viewWidth * CGFloat(column), width: viewWidth) 142 | } 143 | 144 | return horizontal 145 | } 146 | 147 | func rectForRow(at indexPath: IndexPath, threshold: Threshold = .in) -> CGRect { 148 | let vertical = verticalForRow(at: indexPath) 149 | var rect = CGRect(vertical: vertical) 150 | rect.horizontal = horizontalForColumn(indexPath.column) 151 | rect.origin.x += aroundInset.left.width 152 | 153 | switch threshold { 154 | case .below: 155 | rect.origin.x -= validityContentRect.width 156 | case .above: 157 | rect.origin.x += validityContentRect.width 158 | case .in: 159 | break 160 | } 161 | 162 | return rect 163 | } 164 | 165 | func indexPathForRow(at point: CGPoint) -> IndexPath { 166 | let absPoint = CGPoint(x: point.x - aroundInset.left.width, y: point.y) 167 | let absColumn = self.repeat(column(at: absPoint)) 168 | let row = indexForRow(at: absPoint, in: absColumn) 169 | return IndexPath(row: row, column: absColumn) 170 | } 171 | 172 | fileprivate func column(at point: CGPoint) -> Int { 173 | guard let horizontals = horizontals else { 174 | let viewWidth = viewFrame.width * scale.x 175 | guard viewWidth != 0 else { 176 | return 0 177 | } 178 | return Int(floor(point.x / viewWidth)) 179 | } 180 | 181 | var point = point 182 | var column = 0 183 | repeat { 184 | if point.x < 0 { 185 | column += horizontals.endIndex 186 | point.x += validityContentRect.width 187 | } else if point.x >= validityContentRect.width { 188 | column -= horizontals.endIndex 189 | point.x -= validityContentRect.width 190 | } 191 | } while point.x < 0 || point.x >= validityContentRect.width 192 | 193 | for index in horizontals.indices { 194 | let horizontal = (horizontals[index] * scale.x).integral 195 | if horizontal.x <= point.x && horizontal.maxX > point.x { 196 | return index - column 197 | } 198 | } 199 | 200 | fatalError("Column did not get at location \(point).") 201 | } 202 | 203 | fileprivate func indexForRow(at point: CGPoint, in column: Int) -> Int { 204 | let step = 100 205 | let verticals = verticalsForColumn(column) 206 | 207 | for index in stride(from: verticals.startIndex, to: verticals.endIndex, by: step) { 208 | let next = index + step 209 | guard verticals.endIndex <= next || vertical(of: verticals, at: next).maxY * scale.y > point.y else { 210 | continue 211 | } 212 | 213 | for offset in (index.. point.y else { 215 | continue 216 | } 217 | 218 | return offset 219 | } 220 | } 221 | 222 | return 0 223 | } 224 | 225 | func indexesForVisibleColumn(at point: CGPoint) -> [Int] { 226 | var columns: [Int] = [] 227 | guard let visibleSize = visibleSize else { 228 | return columns 229 | } 230 | 231 | let visibleRect = CGRect(origin: CGPoint(x: point.x - aroundInset.left.width, y: 0), size: visibleSize) 232 | let index = column(at: visibleRect.origin) 233 | 234 | var frame = CGRect(origin: .zero, size: viewFrame.size) 235 | for offset in (0.. [Int] { 250 | var rows: [Int] = [] 251 | guard let visibleSize = visibleSize else { 252 | return rows 253 | } 254 | 255 | let visibleRect = CGRect(origin: CGPoint(x: 0, y: point.y), size: visibleSize) 256 | let absColumn: Int 257 | if isInfinitable { 258 | absColumn = self.repeat(column) 259 | } else { 260 | absColumn = column 261 | } 262 | 263 | let index = indexForRow(at: visibleRect.origin, in: absColumn) 264 | let verticals = verticalsForColumn(absColumn) 265 | 266 | var rect: CGRect = .zero 267 | rect.size.width = horizontalForColumn(column).width 268 | for row in (index.. [Vertical?] { 285 | return verticalsForColumn(column) 286 | } 287 | func debugVerticalForRow(at indexPath: IndexPath) -> Vertical { 288 | return verticalForRow(at: indexPath) 289 | } 290 | func debugOffsetXForColumn(_ column: Int) -> CGFloat { 291 | return offsetXForColumn(column) 292 | } 293 | func debugHorizontalForColumn(_ column: Int) -> Horizontal { 294 | return horizontalForColumn(column) 295 | } 296 | func debugColumn(at point: CGPoint) -> Int { 297 | return column(at: point) 298 | } 299 | func debugIndexForRow(at point: CGPoint, in column: Int) -> Int { 300 | return indexForRow(at: point, in: column) 301 | } 302 | } 303 | #endif 304 | -------------------------------------------------------------------------------- /GridView/ViewReference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewReference.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/11/03. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol View: class { 12 | func removeFromSuperview() 13 | } 14 | 15 | extension UIView: View {} 16 | 17 | class ViewReference { 18 | 19 | deinit { 20 | view?.removeFromSuperview() 21 | } 22 | 23 | private(set) weak var view: T? 24 | 25 | init(_ view: T) { 26 | self.view = view 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /GridView/ViewVisibleInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewVisibleInfo.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/11/11. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct ViewVisibleInfo { 12 | private var column: [Int] = [] 13 | private var row: [Int: [Int]] = [:] 14 | private var object: [IndexPath: ViewReference] = [:] 15 | private var selectedIndexPath: Set = [] 16 | 17 | mutating func replaceColumn(_ column: [Int]) { 18 | self.column = column 19 | } 20 | 21 | mutating func replaceRows(_ rows: (Int) -> [Int]) { 22 | for column in self.column { 23 | self.row[column] = rows(column) 24 | } 25 | } 26 | 27 | mutating func replaceObject(with info: ViewVisibleInfo) { 28 | self.object = info.object 29 | } 30 | 31 | mutating func replaceSelectedIndexPath(with info: ViewVisibleInfo) { 32 | self.selectedIndexPath = info.selectedIndexPath 33 | } 34 | 35 | func columns() -> [Int] { 36 | return column 37 | } 38 | 39 | func rows() -> [Int: [Int]] { 40 | return row 41 | } 42 | 43 | func rows(in column: Int) -> [Int] { 44 | return row[column] ?? [] 45 | } 46 | 47 | func visibleObject() -> [IndexPath: ViewReference] { 48 | return object 49 | } 50 | 51 | func isSelected(_ indexPath: IndexPath) -> Bool { 52 | return selectedIndexPath.contains(indexPath) 53 | } 54 | 55 | func object(at indexPath: IndexPath) -> T? { 56 | return object[indexPath]?.view 57 | } 58 | 59 | func indexPathsForSelected() -> [IndexPath] { 60 | return selectedIndexPath.sorted() 61 | } 62 | 63 | mutating func selected(at indexPath: IndexPath) -> T? { 64 | selectedIndexPath.insert(indexPath) 65 | return object[indexPath]?.view 66 | } 67 | 68 | mutating func deselected(at indexPath: IndexPath) -> T? { 69 | selectedIndexPath.remove(indexPath) 70 | return object[indexPath]?.view 71 | } 72 | 73 | mutating func append(_ newObject: T, at indexPath: IndexPath) { 74 | object[indexPath] = ViewReference(newObject) 75 | } 76 | 77 | mutating func removedObject(at indexPath: IndexPath) -> T? { 78 | let oldObject = object[indexPath] 79 | object[indexPath] = nil 80 | return oldObject?.view 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /GridViewExample/GridViewExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C70D53051EC6977700525575 /* PageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C70D53041EC6977700525575 /* PageViewController.swift */; }; 11 | C70D53071EC699FE00525575 /* PageGridViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C70D53061EC699FE00525575 /* PageGridViewCell.swift */; }; 12 | C70D53091EC69A1B00525575 /* PageGridViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C70D53081EC69A1B00525575 /* PageGridViewCell.xib */; }; 13 | C77287631DEABECC005154DE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77287621DEABECC005154DE /* AppDelegate.swift */; }; 14 | C77287651DEABECC005154DE /* TimeTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77287641DEABECC005154DE /* TimeTableViewController.swift */; }; 15 | C77287681DEABECC005154DE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C77287661DEABECC005154DE /* Main.storyboard */; }; 16 | C772876A1DEABECC005154DE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C77287691DEABECC005154DE /* Assets.xcassets */; }; 17 | C772876D1DEABECC005154DE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C772876B1DEABECC005154DE /* LaunchScreen.storyboard */; }; 18 | C772877D1DEABF3C005154DE /* GridView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C772877A1DEABF36005154DE /* GridView.framework */; }; 19 | C772877E1DEABF3C005154DE /* GridView.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C772877A1DEABF36005154DE /* GridView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 20 | C7E9F9A51EC52C1D00D029AF /* DateTimeGridViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7E9F9A41EC52C1D00D029AF /* DateTimeGridViewCell.swift */; }; 21 | C7E9F9A81EC52CCA00D029AF /* DateTimeGridViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C7E9F9A71EC52CCA00D029AF /* DateTimeGridViewCell.xib */; }; 22 | C7E9F9AA1EC52E7C00D029AF /* ChannelListGridViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7E9F9A91EC52E7C00D029AF /* ChannelListGridViewCell.swift */; }; 23 | C7E9F9AC1EC52EA900D029AF /* ChannelListGridViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C7E9F9AB1EC52EA900D029AF /* ChannelListGridViewCell.xib */; }; 24 | C7E9F9AE1EC5325D00D029AF /* TimeTableGridViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7E9F9AD1EC5325D00D029AF /* TimeTableGridViewCell.swift */; }; 25 | C7E9F9B01EC5329100D029AF /* TimeTableGridViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C7E9F9AF1EC5329100D029AF /* TimeTableGridViewCell.xib */; }; 26 | C7E9F9B21EC5383600D029AF /* Slot.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7E9F9B11EC5383600D029AF /* Slot.swift */; }; 27 | /* End PBXBuildFile section */ 28 | 29 | /* Begin PBXContainerItemProxy section */ 30 | C77287791DEABF36005154DE /* PBXContainerItemProxy */ = { 31 | isa = PBXContainerItemProxy; 32 | containerPortal = C77287741DEABF36005154DE /* GridView.xcodeproj */; 33 | proxyType = 2; 34 | remoteGlobalIDString = C77286A01DEAAC26005154DE; 35 | remoteInfo = GridView; 36 | }; 37 | C772877B1DEABF36005154DE /* PBXContainerItemProxy */ = { 38 | isa = PBXContainerItemProxy; 39 | containerPortal = C77287741DEABF36005154DE /* GridView.xcodeproj */; 40 | proxyType = 2; 41 | remoteGlobalIDString = C77286A91DEAAC26005154DE; 42 | remoteInfo = GridViewTests; 43 | }; 44 | C772877F1DEABF3C005154DE /* PBXContainerItemProxy */ = { 45 | isa = PBXContainerItemProxy; 46 | containerPortal = C77287741DEABF36005154DE /* GridView.xcodeproj */; 47 | proxyType = 1; 48 | remoteGlobalIDString = C772869F1DEAAC26005154DE; 49 | remoteInfo = GridView; 50 | }; 51 | /* End PBXContainerItemProxy section */ 52 | 53 | /* Begin PBXCopyFilesBuildPhase section */ 54 | C77287811DEABF3C005154DE /* Embed Frameworks */ = { 55 | isa = PBXCopyFilesBuildPhase; 56 | buildActionMask = 2147483647; 57 | dstPath = ""; 58 | dstSubfolderSpec = 10; 59 | files = ( 60 | C772877E1DEABF3C005154DE /* GridView.framework in Embed Frameworks */, 61 | ); 62 | name = "Embed Frameworks"; 63 | runOnlyForDeploymentPostprocessing = 0; 64 | }; 65 | /* End PBXCopyFilesBuildPhase section */ 66 | 67 | /* Begin PBXFileReference section */ 68 | C70D53041EC6977700525575 /* PageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageViewController.swift; sourceTree = ""; }; 69 | C70D53061EC699FE00525575 /* PageGridViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageGridViewCell.swift; sourceTree = ""; }; 70 | C70D53081EC69A1B00525575 /* PageGridViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PageGridViewCell.xib; sourceTree = ""; }; 71 | C772875F1DEABECC005154DE /* GridViewExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GridViewExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 72 | C77287621DEABECC005154DE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 73 | C77287641DEABECC005154DE /* TimeTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeTableViewController.swift; sourceTree = ""; }; 74 | C77287671DEABECC005154DE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 75 | C77287691DEABECC005154DE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 76 | C772876C1DEABECC005154DE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 77 | C772876E1DEABECC005154DE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 78 | C77287741DEABF36005154DE /* GridView.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = GridView.xcodeproj; path = ../GridView.xcodeproj; sourceTree = ""; }; 79 | C7E9F9A41EC52C1D00D029AF /* DateTimeGridViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateTimeGridViewCell.swift; sourceTree = ""; }; 80 | C7E9F9A71EC52CCA00D029AF /* DateTimeGridViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = DateTimeGridViewCell.xib; sourceTree = ""; }; 81 | C7E9F9A91EC52E7C00D029AF /* ChannelListGridViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelListGridViewCell.swift; sourceTree = ""; }; 82 | C7E9F9AB1EC52EA900D029AF /* ChannelListGridViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ChannelListGridViewCell.xib; sourceTree = ""; }; 83 | C7E9F9AD1EC5325D00D029AF /* TimeTableGridViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeTableGridViewCell.swift; sourceTree = ""; }; 84 | C7E9F9AF1EC5329100D029AF /* TimeTableGridViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TimeTableGridViewCell.xib; sourceTree = ""; }; 85 | C7E9F9B11EC5383600D029AF /* Slot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Slot.swift; sourceTree = ""; }; 86 | /* End PBXFileReference section */ 87 | 88 | /* Begin PBXFrameworksBuildPhase section */ 89 | C772875C1DEABECC005154DE /* Frameworks */ = { 90 | isa = PBXFrameworksBuildPhase; 91 | buildActionMask = 2147483647; 92 | files = ( 93 | C772877D1DEABF3C005154DE /* GridView.framework in Frameworks */, 94 | ); 95 | runOnlyForDeploymentPostprocessing = 0; 96 | }; 97 | /* End PBXFrameworksBuildPhase section */ 98 | 99 | /* Begin PBXGroup section */ 100 | C77287561DEABECC005154DE = { 101 | isa = PBXGroup; 102 | children = ( 103 | C77287741DEABF36005154DE /* GridView.xcodeproj */, 104 | C77287611DEABECC005154DE /* GridViewExample */, 105 | C77287601DEABECC005154DE /* Products */, 106 | ); 107 | sourceTree = ""; 108 | }; 109 | C77287601DEABECC005154DE /* Products */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | C772875F1DEABECC005154DE /* GridViewExample.app */, 113 | ); 114 | name = Products; 115 | sourceTree = ""; 116 | }; 117 | C77287611DEABECC005154DE /* GridViewExample */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | C77287621DEABECC005154DE /* AppDelegate.swift */, 121 | C77287661DEABECC005154DE /* Main.storyboard */, 122 | C77287691DEABECC005154DE /* Assets.xcassets */, 123 | C772876B1DEABECC005154DE /* LaunchScreen.storyboard */, 124 | C772876E1DEABECC005154DE /* Info.plist */, 125 | C7E9F9A91EC52E7C00D029AF /* ChannelListGridViewCell.swift */, 126 | C7E9F9AB1EC52EA900D029AF /* ChannelListGridViewCell.xib */, 127 | C7E9F9A41EC52C1D00D029AF /* DateTimeGridViewCell.swift */, 128 | C7E9F9A71EC52CCA00D029AF /* DateTimeGridViewCell.xib */, 129 | C70D53061EC699FE00525575 /* PageGridViewCell.swift */, 130 | C70D53081EC69A1B00525575 /* PageGridViewCell.xib */, 131 | C70D53041EC6977700525575 /* PageViewController.swift */, 132 | C7E9F9AD1EC5325D00D029AF /* TimeTableGridViewCell.swift */, 133 | C7E9F9AF1EC5329100D029AF /* TimeTableGridViewCell.xib */, 134 | C77287641DEABECC005154DE /* TimeTableViewController.swift */, 135 | C7E9F9B11EC5383600D029AF /* Slot.swift */, 136 | ); 137 | path = GridViewExample; 138 | sourceTree = ""; 139 | }; 140 | C77287751DEABF36005154DE /* Products */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | C772877A1DEABF36005154DE /* GridView.framework */, 144 | C772877C1DEABF36005154DE /* GridViewTests.xctest */, 145 | ); 146 | name = Products; 147 | sourceTree = ""; 148 | }; 149 | /* End PBXGroup section */ 150 | 151 | /* Begin PBXNativeTarget section */ 152 | C772875E1DEABECC005154DE /* GridViewExample */ = { 153 | isa = PBXNativeTarget; 154 | buildConfigurationList = C77287711DEABECC005154DE /* Build configuration list for PBXNativeTarget "GridViewExample" */; 155 | buildPhases = ( 156 | C772875B1DEABECC005154DE /* Sources */, 157 | C772875C1DEABECC005154DE /* Frameworks */, 158 | C772875D1DEABECC005154DE /* Resources */, 159 | C77287811DEABF3C005154DE /* Embed Frameworks */, 160 | ); 161 | buildRules = ( 162 | ); 163 | dependencies = ( 164 | C77287801DEABF3C005154DE /* PBXTargetDependency */, 165 | ); 166 | name = GridViewExample; 167 | productName = GridViewExample; 168 | productReference = C772875F1DEABECC005154DE /* GridViewExample.app */; 169 | productType = "com.apple.product-type.application"; 170 | }; 171 | /* End PBXNativeTarget section */ 172 | 173 | /* Begin PBXProject section */ 174 | C77287571DEABECC005154DE /* Project object */ = { 175 | isa = PBXProject; 176 | attributes = { 177 | LastSwiftUpdateCheck = 0810; 178 | LastUpgradeCheck = 1020; 179 | ORGANIZATIONNAME = "Kyohei Ito"; 180 | TargetAttributes = { 181 | C772875E1DEABECC005154DE = { 182 | CreatedOnToolsVersion = 8.1; 183 | DevelopmentTeam = MX36A76L68; 184 | LastSwiftMigration = 1020; 185 | ProvisioningStyle = Automatic; 186 | }; 187 | }; 188 | }; 189 | buildConfigurationList = C772875A1DEABECC005154DE /* Build configuration list for PBXProject "GridViewExample" */; 190 | compatibilityVersion = "Xcode 3.2"; 191 | developmentRegion = en; 192 | hasScannedForEncodings = 0; 193 | knownRegions = ( 194 | en, 195 | Base, 196 | ); 197 | mainGroup = C77287561DEABECC005154DE; 198 | productRefGroup = C77287601DEABECC005154DE /* Products */; 199 | projectDirPath = ""; 200 | projectReferences = ( 201 | { 202 | ProductGroup = C77287751DEABF36005154DE /* Products */; 203 | ProjectRef = C77287741DEABF36005154DE /* GridView.xcodeproj */; 204 | }, 205 | ); 206 | projectRoot = ""; 207 | targets = ( 208 | C772875E1DEABECC005154DE /* GridViewExample */, 209 | ); 210 | }; 211 | /* End PBXProject section */ 212 | 213 | /* Begin PBXReferenceProxy section */ 214 | C772877A1DEABF36005154DE /* GridView.framework */ = { 215 | isa = PBXReferenceProxy; 216 | fileType = wrapper.framework; 217 | path = GridView.framework; 218 | remoteRef = C77287791DEABF36005154DE /* PBXContainerItemProxy */; 219 | sourceTree = BUILT_PRODUCTS_DIR; 220 | }; 221 | C772877C1DEABF36005154DE /* GridViewTests.xctest */ = { 222 | isa = PBXReferenceProxy; 223 | fileType = wrapper.cfbundle; 224 | path = GridViewTests.xctest; 225 | remoteRef = C772877B1DEABF36005154DE /* PBXContainerItemProxy */; 226 | sourceTree = BUILT_PRODUCTS_DIR; 227 | }; 228 | /* End PBXReferenceProxy section */ 229 | 230 | /* Begin PBXResourcesBuildPhase section */ 231 | C772875D1DEABECC005154DE /* Resources */ = { 232 | isa = PBXResourcesBuildPhase; 233 | buildActionMask = 2147483647; 234 | files = ( 235 | C7E9F9AC1EC52EA900D029AF /* ChannelListGridViewCell.xib in Resources */, 236 | C7E9F9B01EC5329100D029AF /* TimeTableGridViewCell.xib in Resources */, 237 | C772876D1DEABECC005154DE /* LaunchScreen.storyboard in Resources */, 238 | C772876A1DEABECC005154DE /* Assets.xcassets in Resources */, 239 | C7E9F9A81EC52CCA00D029AF /* DateTimeGridViewCell.xib in Resources */, 240 | C77287681DEABECC005154DE /* Main.storyboard in Resources */, 241 | C70D53091EC69A1B00525575 /* PageGridViewCell.xib in Resources */, 242 | ); 243 | runOnlyForDeploymentPostprocessing = 0; 244 | }; 245 | /* End PBXResourcesBuildPhase section */ 246 | 247 | /* Begin PBXSourcesBuildPhase section */ 248 | C772875B1DEABECC005154DE /* Sources */ = { 249 | isa = PBXSourcesBuildPhase; 250 | buildActionMask = 2147483647; 251 | files = ( 252 | C77287651DEABECC005154DE /* TimeTableViewController.swift in Sources */, 253 | C70D53051EC6977700525575 /* PageViewController.swift in Sources */, 254 | C77287631DEABECC005154DE /* AppDelegate.swift in Sources */, 255 | C7E9F9B21EC5383600D029AF /* Slot.swift in Sources */, 256 | C7E9F9A51EC52C1D00D029AF /* DateTimeGridViewCell.swift in Sources */, 257 | C7E9F9AE1EC5325D00D029AF /* TimeTableGridViewCell.swift in Sources */, 258 | C7E9F9AA1EC52E7C00D029AF /* ChannelListGridViewCell.swift in Sources */, 259 | C70D53071EC699FE00525575 /* PageGridViewCell.swift in Sources */, 260 | ); 261 | runOnlyForDeploymentPostprocessing = 0; 262 | }; 263 | /* End PBXSourcesBuildPhase section */ 264 | 265 | /* Begin PBXTargetDependency section */ 266 | C77287801DEABF3C005154DE /* PBXTargetDependency */ = { 267 | isa = PBXTargetDependency; 268 | name = GridView; 269 | targetProxy = C772877F1DEABF3C005154DE /* PBXContainerItemProxy */; 270 | }; 271 | /* End PBXTargetDependency section */ 272 | 273 | /* Begin PBXVariantGroup section */ 274 | C77287661DEABECC005154DE /* Main.storyboard */ = { 275 | isa = PBXVariantGroup; 276 | children = ( 277 | C77287671DEABECC005154DE /* Base */, 278 | ); 279 | name = Main.storyboard; 280 | sourceTree = ""; 281 | }; 282 | C772876B1DEABECC005154DE /* LaunchScreen.storyboard */ = { 283 | isa = PBXVariantGroup; 284 | children = ( 285 | C772876C1DEABECC005154DE /* Base */, 286 | ); 287 | name = LaunchScreen.storyboard; 288 | sourceTree = ""; 289 | }; 290 | /* End PBXVariantGroup section */ 291 | 292 | /* Begin XCBuildConfiguration section */ 293 | C772876F1DEABECC005154DE /* Debug */ = { 294 | isa = XCBuildConfiguration; 295 | buildSettings = { 296 | ALWAYS_SEARCH_USER_PATHS = NO; 297 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 298 | CLANG_ANALYZER_NONNULL = YES; 299 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 300 | CLANG_CXX_LIBRARY = "libc++"; 301 | CLANG_ENABLE_MODULES = YES; 302 | CLANG_ENABLE_OBJC_ARC = YES; 303 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 304 | CLANG_WARN_BOOL_CONVERSION = YES; 305 | CLANG_WARN_COMMA = YES; 306 | CLANG_WARN_CONSTANT_CONVERSION = YES; 307 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 308 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 309 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 310 | CLANG_WARN_EMPTY_BODY = YES; 311 | CLANG_WARN_ENUM_CONVERSION = YES; 312 | CLANG_WARN_INFINITE_RECURSION = YES; 313 | CLANG_WARN_INT_CONVERSION = YES; 314 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 315 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 316 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 317 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 318 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 319 | CLANG_WARN_STRICT_PROTOTYPES = YES; 320 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 321 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 322 | CLANG_WARN_UNREACHABLE_CODE = YES; 323 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 324 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 325 | COPY_PHASE_STRIP = NO; 326 | DEBUG_INFORMATION_FORMAT = dwarf; 327 | ENABLE_STRICT_OBJC_MSGSEND = YES; 328 | ENABLE_TESTABILITY = YES; 329 | GCC_C_LANGUAGE_STANDARD = gnu99; 330 | GCC_DYNAMIC_NO_PIC = NO; 331 | GCC_NO_COMMON_BLOCKS = YES; 332 | GCC_OPTIMIZATION_LEVEL = 0; 333 | GCC_PREPROCESSOR_DEFINITIONS = ( 334 | "DEBUG=1", 335 | "$(inherited)", 336 | ); 337 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 338 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 339 | GCC_WARN_UNDECLARED_SELECTOR = YES; 340 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 341 | GCC_WARN_UNUSED_FUNCTION = YES; 342 | GCC_WARN_UNUSED_VARIABLE = YES; 343 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 344 | MTL_ENABLE_DEBUG_INFO = YES; 345 | ONLY_ACTIVE_ARCH = YES; 346 | SDKROOT = iphoneos; 347 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 348 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 349 | }; 350 | name = Debug; 351 | }; 352 | C77287701DEABECC005154DE /* Release */ = { 353 | isa = XCBuildConfiguration; 354 | buildSettings = { 355 | ALWAYS_SEARCH_USER_PATHS = NO; 356 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 357 | CLANG_ANALYZER_NONNULL = YES; 358 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 359 | CLANG_CXX_LIBRARY = "libc++"; 360 | CLANG_ENABLE_MODULES = YES; 361 | CLANG_ENABLE_OBJC_ARC = YES; 362 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 363 | CLANG_WARN_BOOL_CONVERSION = YES; 364 | CLANG_WARN_COMMA = YES; 365 | CLANG_WARN_CONSTANT_CONVERSION = YES; 366 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 367 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 368 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 369 | CLANG_WARN_EMPTY_BODY = YES; 370 | CLANG_WARN_ENUM_CONVERSION = YES; 371 | CLANG_WARN_INFINITE_RECURSION = YES; 372 | CLANG_WARN_INT_CONVERSION = YES; 373 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 374 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 375 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 376 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 377 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 378 | CLANG_WARN_STRICT_PROTOTYPES = YES; 379 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 380 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 381 | CLANG_WARN_UNREACHABLE_CODE = YES; 382 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 383 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 384 | COPY_PHASE_STRIP = NO; 385 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 386 | ENABLE_NS_ASSERTIONS = NO; 387 | ENABLE_STRICT_OBJC_MSGSEND = YES; 388 | GCC_C_LANGUAGE_STANDARD = gnu99; 389 | GCC_NO_COMMON_BLOCKS = YES; 390 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 391 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 392 | GCC_WARN_UNDECLARED_SELECTOR = YES; 393 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 394 | GCC_WARN_UNUSED_FUNCTION = YES; 395 | GCC_WARN_UNUSED_VARIABLE = YES; 396 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 397 | MTL_ENABLE_DEBUG_INFO = NO; 398 | SDKROOT = iphoneos; 399 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 400 | VALIDATE_PRODUCT = YES; 401 | }; 402 | name = Release; 403 | }; 404 | C77287721DEABECC005154DE /* Debug */ = { 405 | isa = XCBuildConfiguration; 406 | buildSettings = { 407 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 408 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 409 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 410 | CODE_SIGN_STYLE = Automatic; 411 | DEVELOPMENT_TEAM = MX36A76L68; 412 | INFOPLIST_FILE = GridViewExample/Info.plist; 413 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 414 | PRODUCT_BUNDLE_IDENTIFIER = com.kyoheiito.GridViewExample; 415 | PRODUCT_NAME = "$(TARGET_NAME)"; 416 | PROVISIONING_PROFILE_SPECIFIER = ""; 417 | SWIFT_VERSION = 5.0; 418 | }; 419 | name = Debug; 420 | }; 421 | C77287731DEABECC005154DE /* Release */ = { 422 | isa = XCBuildConfiguration; 423 | buildSettings = { 424 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 425 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 426 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 427 | CODE_SIGN_STYLE = Automatic; 428 | DEVELOPMENT_TEAM = MX36A76L68; 429 | INFOPLIST_FILE = GridViewExample/Info.plist; 430 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 431 | PRODUCT_BUNDLE_IDENTIFIER = com.kyoheiito.GridViewExample; 432 | PRODUCT_NAME = "$(TARGET_NAME)"; 433 | PROVISIONING_PROFILE_SPECIFIER = ""; 434 | SWIFT_VERSION = 5.0; 435 | }; 436 | name = Release; 437 | }; 438 | /* End XCBuildConfiguration section */ 439 | 440 | /* Begin XCConfigurationList section */ 441 | C772875A1DEABECC005154DE /* Build configuration list for PBXProject "GridViewExample" */ = { 442 | isa = XCConfigurationList; 443 | buildConfigurations = ( 444 | C772876F1DEABECC005154DE /* Debug */, 445 | C77287701DEABECC005154DE /* Release */, 446 | ); 447 | defaultConfigurationIsVisible = 0; 448 | defaultConfigurationName = Release; 449 | }; 450 | C77287711DEABECC005154DE /* Build configuration list for PBXNativeTarget "GridViewExample" */ = { 451 | isa = XCConfigurationList; 452 | buildConfigurations = ( 453 | C77287721DEABECC005154DE /* Debug */, 454 | C77287731DEABECC005154DE /* Release */, 455 | ); 456 | defaultConfigurationIsVisible = 0; 457 | defaultConfigurationName = Release; 458 | }; 459 | /* End XCConfigurationList section */ 460 | }; 461 | rootObject = C77287571DEABECC005154DE /* Project object */; 462 | } 463 | -------------------------------------------------------------------------------- /GridViewExample/GridViewExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /GridViewExample/GridViewExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /GridViewExample/GridViewExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // GridViewExample 4 | // 5 | // Created by Kyohei Ito on 2016/10/30. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /GridViewExample/GridViewExample/Assets.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 | } -------------------------------------------------------------------------------- /GridViewExample/GridViewExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /GridViewExample/GridViewExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 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 | -------------------------------------------------------------------------------- /GridViewExample/GridViewExample/ChannelListGridViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChannelListGridViewCell.swift 3 | // GridViewExample 4 | // 5 | // Created by Kyohei Ito on 2017/05/11. 6 | // Copyright © 2017年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import GridView 10 | 11 | class ChannelListGridViewCell: GridViewCell { 12 | @IBOutlet weak var channelLabel: UILabel! 13 | 14 | static var nib: UINib { 15 | return UINib(nibName: "ChannelListGridViewCell", bundle: Bundle(for: self)) 16 | } 17 | 18 | override func awakeFromNib() { 19 | super.awakeFromNib() 20 | 21 | channelLabel.font = .boldSystemFont(ofSize: 16) 22 | channelLabel.textColor = .lightGray 23 | channelLabel.textAlignment = .center 24 | } 25 | 26 | func configure(_ channelName: String) { 27 | channelLabel.text = channelName 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /GridViewExample/GridViewExample/ChannelListGridViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /GridViewExample/GridViewExample/DateTimeGridViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateTimeGridViewCell.swift 3 | // GridViewExample 4 | // 5 | // Created by Kyohei Ito on 2017/05/11. 6 | // Copyright © 2017年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import GridView 11 | 12 | class DateTimeGridViewCell: GridViewCell { 13 | @IBOutlet weak var timeLabel: UILabel! 14 | @IBOutlet weak var borderView: UIView! 15 | 16 | static var nib: UINib { 17 | return UINib(nibName: "DateTimeGridViewCell", bundle: Bundle(for: self)) 18 | } 19 | 20 | override func awakeFromNib() { 21 | super.awakeFromNib() 22 | 23 | timeLabel.font = .boldSystemFont(ofSize: 14) 24 | timeLabel.textColor = .white 25 | timeLabel.numberOfLines = 1 26 | timeLabel.textAlignment = .center 27 | 28 | borderView.backgroundColor = .white 29 | } 30 | 31 | func configure(_ hour: Int) { 32 | timeLabel.text = "\(hour)" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /GridViewExample/GridViewExample/DateTimeGridViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /GridViewExample/GridViewExample/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 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /GridViewExample/GridViewExample/PageGridViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageGridViewCell.swift 3 | // GridViewExample 4 | // 5 | // Created by Kyohei Ito on 2017/05/12. 6 | // Copyright © 2017年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import GridView 11 | 12 | class PageGridViewCell: GridViewCell { 13 | @IBOutlet weak var label: UILabel! 14 | 15 | static var nib: UINib { 16 | return UINib(nibName: "PageGridViewCell", bundle: Bundle(for: self)) 17 | } 18 | 19 | override func awakeFromNib() { 20 | super.awakeFromNib() 21 | 22 | layer.borderColor = UIColor.gray.cgColor 23 | layer.borderWidth = 2 24 | 25 | label.font = .boldSystemFont(ofSize: 36) 26 | label.textColor = UIColor.lightGray 27 | label.textAlignment = .center 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /GridViewExample/GridViewExample/PageGridViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /GridViewExample/GridViewExample/PageViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageViewController.swift 3 | // GridViewExample 4 | // 5 | // Created by Kyohei Ito on 2017/05/12. 6 | // Copyright © 2017年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import GridView 11 | 12 | class PageViewController: UIViewController { 13 | @IBOutlet weak var gridView: GridView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | gridView.register(PageGridViewCell.nib, forCellWithReuseIdentifier: "PageGridViewCell") 19 | gridView.dataSource = self 20 | gridView.delegate = self 21 | gridView.isPagingEnabled = true 22 | gridView.isDirectionalLockEnabled = true 23 | gridView.minimumScale = Scale(x: 1/3, y: 1/3) 24 | } 25 | 26 | override func viewDidLayoutSubviews() { 27 | super.viewDidLayoutSubviews() 28 | 29 | let frame = gridView.frame 30 | gridView.contentInset = UIEdgeInsets(top: frame.minY, left: frame.minX, bottom: view.bounds.height - frame.maxY, right: view.bounds.width - frame.maxX) 31 | gridView.scrollIndicatorInsets = gridView.contentInset 32 | } 33 | 34 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 35 | super.viewWillTransition(to: size, with: coordinator) 36 | 37 | coordinator.animate(alongsideTransition: { _ in 38 | self.gridView.invalidateContentSize() 39 | self.view.layoutIfNeeded() 40 | self.adjustStateCells() 41 | }) 42 | } 43 | 44 | func adjustStateCells() { 45 | gridView.visibleCells().forEach { (cell: PageGridViewCell) in 46 | let cellFrame = cell.convert(cell.bounds, to: view) 47 | let centerX = view.frame.midX - cellFrame.midX 48 | let centerY = view.frame.midY - cellFrame.midY 49 | let distanceRatio = 1 - min(1, abs(centerX / view.bounds.width) + abs(centerY / view.bounds.height)) 50 | 51 | let color = 0.5 * distanceRatio 52 | cell.backgroundColor = UIColor(red: color, green: 0, blue: 0.5 - color, alpha: 1) 53 | } 54 | } 55 | } 56 | 57 | extension PageViewController: GridViewDataSource, GridViewDelegate { 58 | func numberOfColumns(in gridView: GridView) -> Int { 59 | return 30 60 | } 61 | 62 | func gridView(_ gridView: GridView, numberOfRowsInColumn column: Int) -> Int { 63 | return 30 64 | } 65 | 66 | func gridView(_ gridView: GridView, cellForRowAt indexPath: IndexPath) -> GridViewCell { 67 | let cell = gridView.dequeueReusableCell(withReuseIdentifier: "PageGridViewCell", for: indexPath) 68 | if let cell = cell as? PageGridViewCell { 69 | cell.label.text = "\(indexPath.column) - \(indexPath.row)" 70 | } 71 | 72 | return cell 73 | } 74 | 75 | func gridView(_ gridView: GridView, didSelectRowAt indexPath: IndexPath) { 76 | gridView.scrollToRow(at: indexPath, at: [.topFit, .leftFit], animated: true) 77 | } 78 | 79 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 80 | adjustStateCells() 81 | } 82 | 83 | func gridView(_ gridView: GridView, didScaleAt scale: CGFloat) { 84 | adjustStateCells() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /GridViewExample/GridViewExample/Slot.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Slot.swift 3 | // GridViewExample 4 | // 5 | // Created by Kyohei Ito on 2017/05/11. 6 | // Copyright © 2017年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Slot { 12 | let minutes: Int 13 | let startAt: Int 14 | let title: String 15 | let detail: String 16 | } 17 | -------------------------------------------------------------------------------- /GridViewExample/GridViewExample/TimeTableGridViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeTableGridViewCell.swift 3 | // GridViewExample 4 | // 5 | // Created by Kyohei Ito on 2017/05/11. 6 | // Copyright © 2017年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import GridView 10 | 11 | class TimeTableGridViewCell: GridViewCell { 12 | @IBOutlet weak var timeLabel: UILabel! 13 | @IBOutlet weak var titleLabel: UILabel! 14 | @IBOutlet weak var detailLabel: UILabel! 15 | 16 | static var nib: UINib { 17 | return UINib(nibName: "TimeTableGridViewCell", bundle: Bundle(for: self)) 18 | } 19 | 20 | override func prepareForReuse() { 21 | super.prepareForReuse() 22 | 23 | timeLabel.text = nil 24 | titleLabel.text = nil 25 | detailLabel.text = nil 26 | } 27 | 28 | override func awakeFromNib() { 29 | super.awakeFromNib() 30 | 31 | clipsToBounds = true 32 | layer.borderColor = UIColor.gray.cgColor 33 | layer.borderWidth = 1 / UIScreen.main.scale 34 | 35 | timeLabel.font = .boldSystemFont(ofSize: 10) 36 | timeLabel.textAlignment = .center 37 | 38 | titleLabel.font = .boldSystemFont(ofSize: 10) 39 | titleLabel.textColor = UIColor.black 40 | titleLabel.numberOfLines = 4 41 | titleLabel.textAlignment = .left 42 | 43 | detailLabel.font = .systemFont(ofSize: 10) 44 | detailLabel.textColor = UIColor.darkGray 45 | detailLabel.textAlignment = .left 46 | } 47 | 48 | func configure(_ slot: Slot) { 49 | timeLabel.text = String(format: "%02d", slot.startAt % 60) 50 | titleLabel.text = slot.title 51 | detailLabel.text = slot.detail 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /GridViewExample/GridViewExample/TimeTableGridViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 28 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /GridViewExample/GridViewExample/TimeTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // GridViewExample 4 | // 5 | // Created by Kyohei Ito on 2016/10/30. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import GridView 11 | 12 | extension UIColor { 13 | fileprivate convenience init(hex: Int, alpha: CGFloat = 1) { 14 | let red = CGFloat((hex & 0xFF0000) >> 16) / 255 15 | let green = CGFloat((hex & 0x00FF00) >> 8 ) / 255 16 | let blue = CGFloat((hex & 0x0000FF) >> 0 ) / 255 17 | 18 | self.init(red: red, green: green, blue: blue, alpha: alpha) 19 | } 20 | } 21 | 22 | class TimeTableViewController: UIViewController { 23 | @IBOutlet weak var timeTableView: GridView! 24 | @IBOutlet weak var channelListView: GridView! 25 | @IBOutlet weak var dateTimeView: GridView! 26 | 27 | private let channels: [String] = ["News", "Anime", "Drama", "MTV", "Music", "Pets", "Documentary", "Soccer", "Cooking", "Gourmet", "Extreme", "Esports"] 28 | 29 | fileprivate lazy var slotList: [[Slot]] = { 30 | let detailText = "Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda." 31 | let minutesOfDay = 24 * 60 32 | let frames = [15, 15, 20, 20, 30, 30, 40, 40, 50, 50, 60, 60, 75, 75, 90, 90] 33 | return self.channels.enumerated().map { index, channel in 34 | var slots: [Slot] = [] 35 | var totalMinutes = 0 36 | while totalMinutes < minutesOfDay { 37 | var minutes = frames[Int(arc4random_uniform(UInt32(frames.count)))] 38 | let startAt = totalMinutes + minutes 39 | minutes -= max(startAt - minutesOfDay, 0) 40 | let slot = Slot(minutes: minutes, startAt: totalMinutes, title: "\(channel)'s slot", detail: detailText) 41 | totalMinutes = startAt 42 | slots.append(slot) 43 | } 44 | return slots 45 | } 46 | }() 47 | 48 | private lazy var channelListDataSource: ChannelListDataSource = .init(channels: self.channels) 49 | private let dateTimeDataSource = DateTimeGridViewDataSource() 50 | 51 | override func viewDidLoad() { 52 | super.viewDidLoad() 53 | 54 | view.backgroundColor = .black 55 | view.layoutIfNeeded() 56 | 57 | timeTableView.register(TimeTableGridViewCell.nib, forCellWithReuseIdentifier: "TimeTableGridViewCell") 58 | channelListView.register(ChannelListGridViewCell.nib, forCellWithReuseIdentifier: "ChannelListGridViewCell") 59 | dateTimeView.register(DateTimeGridViewCell.nib, forCellWithReuseIdentifier: "DateTimeGridViewCell") 60 | 61 | // timeTableView.layoutWithoutFillForCell = true 62 | timeTableView.superview?.clipsToBounds = true 63 | timeTableView.contentInset.top = channelListView.bounds.height 64 | timeTableView.minimumScale = Scale(x: 0.5, y: 0.5) 65 | timeTableView.maximumScale = Scale(x: 1.5, y: 1.5) 66 | timeTableView.scrollIndicatorInsets.top = timeTableView.contentInset.top 67 | timeTableView.scrollIndicatorInsets.left = dateTimeView.bounds.width 68 | timeTableView.dataSource = self 69 | timeTableView.delegate = self 70 | timeTableView.reloadData() 71 | 72 | // channelListView.layoutWithoutFillForCell = true 73 | channelListView.superview?.backgroundColor = .black 74 | channelListView.superview?.isUserInteractionEnabled = false 75 | channelListView.minimumScale.x = timeTableView.minimumScale.x 76 | channelListView.maximumScale.x = timeTableView.maximumScale.x 77 | channelListView.dataSource = channelListDataSource 78 | channelListView.delegate = channelListDataSource 79 | channelListView.reloadData() 80 | 81 | dateTimeView.superview?.clipsToBounds = true 82 | dateTimeView.superview?.backgroundColor = UIColor(hex: 0x6FB900) 83 | dateTimeView.superview?.isUserInteractionEnabled = false 84 | dateTimeView.contentInset.top = channelListView.bounds.height 85 | dateTimeView.minimumScale.y = timeTableView.minimumScale.y 86 | dateTimeView.maximumScale.y = timeTableView.maximumScale.y 87 | dateTimeView.isInfinitable = false 88 | dateTimeView.dataSource = dateTimeDataSource 89 | dateTimeView.delegate = dateTimeDataSource 90 | dateTimeView.reloadData() 91 | } 92 | 93 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 94 | super.viewWillTransition(to: size, with: coordinator) 95 | 96 | coordinator.animate(alongsideTransition: { _ in 97 | self.timeTableView.invalidateContentSize() 98 | self.channelListView.invalidateContentSize() 99 | self.view.layoutIfNeeded() 100 | }) 101 | } 102 | } 103 | 104 | extension TimeTableViewController: GridViewDataSource, GridViewDelegate { 105 | func numberOfColumns(in gridView: GridView) -> Int { 106 | return slotList.count 107 | } 108 | 109 | func gridView(_ gridView: GridView, numberOfRowsInColumn column: Int) -> Int { 110 | return slotList[column].count 111 | } 112 | 113 | func gridView(_ gridView: GridView, heightForRowAt indexPath: IndexPath) -> CGFloat { 114 | return CGFloat(slotList[indexPath.column][indexPath.row].minutes * 2) 115 | } 116 | 117 | func gridView(_ gridView: GridView, cellForRowAt indexPath: IndexPath) -> GridViewCell { 118 | let cell = gridView.dequeueReusableCell(withReuseIdentifier: "TimeTableGridViewCell", for: indexPath) 119 | if let cell = cell as? TimeTableGridViewCell { 120 | cell.configure(slotList[indexPath.column][indexPath.row]) 121 | } 122 | 123 | return cell 124 | } 125 | 126 | func gridView(_ gridView: GridView, didScaleAt scale: CGFloat) { 127 | channelListView.contentScale(scale) 128 | dateTimeView.contentScale(scale) 129 | } 130 | 131 | func gridView(_ gridView: GridView, didSelectRowAt indexPath: IndexPath) { 132 | gridView.deselectRow(at: indexPath) 133 | gridView.scrollToRow(at: indexPath, at: [.topFit, .centeredHorizontally], animated: true) 134 | } 135 | 136 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 137 | channelListView.contentOffset.x = scrollView.contentOffset.x 138 | dateTimeView.contentOffset.y = scrollView.contentOffset.y 139 | } 140 | } 141 | 142 | final class DateTimeGridViewDataSource: NSObject, GridViewDataSource, GridViewDelegate { 143 | func gridView(_ gridView: GridView, numberOfRowsInColumn column: Int) -> Int { 144 | return 24 145 | } 146 | 147 | func gridView(_ gridView: GridView, heightForRowAt indexPath: IndexPath) -> CGFloat { 148 | return 60 * 2 149 | } 150 | 151 | func gridView(_ gridView: GridView, cellForRowAt indexPath: IndexPath) -> GridViewCell { 152 | let cell = gridView.dequeueReusableCell(withReuseIdentifier: "DateTimeGridViewCell", for: indexPath) 153 | if let cell = cell as? DateTimeGridViewCell { 154 | cell.configure(indexPath.row) 155 | } 156 | 157 | return cell 158 | } 159 | } 160 | 161 | final class ChannelListDataSource: NSObject, GridViewDataSource, GridViewDelegate { 162 | let channels: [String] 163 | 164 | init(channels: [String]) { 165 | self.channels = channels 166 | } 167 | 168 | func numberOfColumns(in gridView: GridView) -> Int { 169 | return channels.count 170 | } 171 | 172 | func gridView(_ gridView: GridView, numberOfRowsInColumn column: Int) -> Int { 173 | return 1 174 | } 175 | 176 | func gridView(_ gridView: GridView, cellForRowAt indexPath: IndexPath) -> GridViewCell { 177 | let cell = gridView.dequeueReusableCell(withReuseIdentifier: "ChannelListGridViewCell", for: indexPath) 178 | if let cell = cell as? ChannelListGridViewCell { 179 | cell.configure(channels[indexPath.column]) 180 | } 181 | 182 | return cell 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /GridViewTests/AroundInsetsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AroundInsetsTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/29. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class AroundInsetsTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testInit() { 25 | let parentSize = CGSize(width: 300, height: 300) 26 | let inset1 = AroundInsets(parentSize: parentSize, frame: CGRect(origin: CGPoint(x: 135, y: 135), size: CGSize(width: 30, height: 30))) 27 | 28 | XCTAssertEqual(inset1.left.width.truncatingRemainder(dividingBy: 30), 0) 29 | XCTAssertEqual(inset1.right.width.truncatingRemainder(dividingBy: 30), 0) 30 | XCTAssertEqual(inset1.left.width / 30, 5) 31 | XCTAssertEqual(inset1.right.width / 30, 5) 32 | 33 | let inset2 = AroundInsets(parentSize: parentSize, frame: CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: 100, height: 100))) 34 | 35 | XCTAssertEqual(inset2.left.width.truncatingRemainder(dividingBy: 100), 0) 36 | XCTAssertEqual(inset2.right.width.truncatingRemainder(dividingBy: 100), 0) 37 | XCTAssertEqual(inset2.left.width / 100, 1) 38 | XCTAssertEqual(inset2.right.width / 100, 1) 39 | } 40 | 41 | func testZero() { 42 | XCTAssertEqual(AroundInsets.zero.left.width, 0) 43 | XCTAssertEqual(AroundInsets.zero.right.width, 0) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /GridViewTests/ArrayExtensionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayExtensionTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/29. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class ArrayExtensionTests: XCTestCase { 13 | let array1 = [0,1,2,3,4,5,6,7,8,9] 14 | let array2 = [0,2,4,6,8] 15 | let array3 = [1,3,5,7,9] 16 | 17 | override func setUp() { 18 | super.setUp() 19 | // Put setup code here. This method is called before the invocation of each test method in the class. 20 | } 21 | 22 | override func tearDown() { 23 | // Put teardown code here. This method is called after the invocation of each test method in the class. 24 | super.tearDown() 25 | } 26 | 27 | func testUnion() { 28 | XCTAssertEqual(array1.union(array1), [0,1,2,3,4,5,6,7,8,9]) 29 | XCTAssertEqual(array1.union(array2), [0,1,2,3,4,5,6,7,8,9]) 30 | XCTAssertEqual(array1.union(array3), [0,1,2,3,4,5,6,7,8,9]) 31 | 32 | XCTAssertEqual(array2.union(array1), [0,2,4,6,8,1,3,5,7,9]) 33 | XCTAssertEqual(array2.union(array2), [0,2,4,6,8]) 34 | XCTAssertEqual(array2.union(array3), [0,2,4,6,8,1,3,5,7,9]) 35 | 36 | XCTAssertEqual(array3.union(array1), [1,3,5,7,9,0,2,4,6,8]) 37 | XCTAssertEqual(array3.union(array2), [1,3,5,7,9,0,2,4,6,8]) 38 | XCTAssertEqual(array3.union(array3), [1,3,5,7,9]) 39 | } 40 | 41 | func testSubtracting() { 42 | XCTAssertEqual(array1.subtracting(array1), []) 43 | XCTAssertEqual(array1.subtracting(array2), [1,3,5,7,9]) 44 | XCTAssertEqual(array1.subtracting(array3), [0,2,4,6,8]) 45 | 46 | XCTAssertEqual(array2.subtracting(array1), []) 47 | XCTAssertEqual(array2.subtracting(array2), []) 48 | XCTAssertEqual(array2.subtracting(array3), [0,2,4,6,8]) 49 | 50 | XCTAssertEqual(array3.subtracting(array1), []) 51 | XCTAssertEqual(array3.subtracting(array2), [1,3,5,7,9]) 52 | XCTAssertEqual(array3.subtracting(array3), []) 53 | } 54 | 55 | func testIntersection() { 56 | XCTAssertEqual(array1.intersection(array1), [0,1,2,3,4,5,6,7,8,9]) 57 | XCTAssertEqual(array1.intersection(array2), [0,2,4,6,8]) 58 | XCTAssertEqual(array1.intersection(array3), [1,3,5,7,9]) 59 | 60 | XCTAssertEqual(array2.intersection(array1), [0,2,4,6,8]) 61 | XCTAssertEqual(array2.intersection(array2), [0,2,4,6,8]) 62 | XCTAssertEqual(array2.intersection(array3), []) 63 | 64 | XCTAssertEqual(array3.intersection(array1), [1,3,5,7,9]) 65 | XCTAssertEqual(array3.intersection(array2), []) 66 | XCTAssertEqual(array3.intersection(array3), [1,3,5,7,9]) 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /GridViewTests/CGFloatExtensionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGFloatExtensionTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2017/01/02. 6 | // Copyright © 2017年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class CGFloatExtensionTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testIntegral() { 25 | CGFloat.debugScale = 2 26 | XCTAssertEqual(CGFloat.debugScale, 2) 27 | XCTAssertEqual(CGFloat(10.0).integral, 10.0) 28 | XCTAssertEqual(CGFloat(10.1).integral, 10.0) 29 | XCTAssertEqual(CGFloat(10.2).integral, 10.0) 30 | XCTAssertEqual(CGFloat(10.3).integral, 10.5) 31 | XCTAssertEqual(CGFloat(10.4).integral, 10.5) 32 | XCTAssertEqual(CGFloat(10.5).integral, 10.5) 33 | XCTAssertEqual(CGFloat(10.6).integral, 10.5) 34 | XCTAssertEqual(CGFloat(10.7).integral, 10.5) 35 | XCTAssertEqual(CGFloat(10.8).integral, 11) 36 | XCTAssertEqual(CGFloat(10.9).integral, 11) 37 | XCTAssertEqual(CGFloat(-10.0).integral, -10.0) 38 | XCTAssertEqual(CGFloat(-10.1).integral, -10.0) 39 | XCTAssertEqual(CGFloat(-10.2).integral, -10.0) 40 | XCTAssertEqual(CGFloat(-10.3).integral, -10.5) 41 | XCTAssertEqual(CGFloat(-10.4).integral, -10.5) 42 | XCTAssertEqual(CGFloat(-10.5).integral, -10.5) 43 | XCTAssertEqual(CGFloat(-10.6).integral, -10.5) 44 | XCTAssertEqual(CGFloat(-10.7).integral, -10.5) 45 | XCTAssertEqual(CGFloat(-10.8).integral, -11) 46 | XCTAssertEqual(CGFloat(-10.9).integral, -11) 47 | 48 | CGFloat.debugScale = 3 49 | XCTAssertEqual(CGFloat.debugScale, 3) 50 | XCTAssertEqual(CGFloat(10.0).integral.rounded(p: 5), 10.0) 51 | XCTAssertEqual(CGFloat(10.1).integral.rounded(p: 5), 10.0) 52 | XCTAssertEqual(CGFloat(10.2).integral.rounded(p: 5), 10.33333) 53 | XCTAssertEqual(CGFloat(10.3).integral.rounded(p: 5), 10.33333) 54 | XCTAssertEqual(CGFloat(10.4).integral.rounded(p: 5), 10.33333) 55 | XCTAssertEqual(CGFloat(10.5).integral.rounded(p: 5), 10.66667) 56 | XCTAssertEqual(CGFloat(10.6).integral.rounded(p: 5), 10.66667) 57 | XCTAssertEqual(CGFloat(10.7).integral.rounded(p: 5), 10.66667) 58 | XCTAssertEqual(CGFloat(10.8).integral.rounded(p: 5), 10.66667) 59 | XCTAssertEqual(CGFloat(10.9).integral.rounded(p: 5), 11) 60 | XCTAssertEqual(CGFloat(-10.0).integral.rounded(p: 5), -10.0) 61 | XCTAssertEqual(CGFloat(-10.1).integral.rounded(p: 5), -10.0) 62 | XCTAssertEqual(CGFloat(-10.2).integral.rounded(p: 5), -10.33333) 63 | XCTAssertEqual(CGFloat(-10.3).integral.rounded(p: 5), -10.33333) 64 | XCTAssertEqual(CGFloat(-10.4).integral.rounded(p: 5), -10.33333) 65 | XCTAssertEqual(CGFloat(-10.5).integral.rounded(p: 5), -10.66667) 66 | XCTAssertEqual(CGFloat(-10.6).integral.rounded(p: 5), -10.66667) 67 | XCTAssertEqual(CGFloat(-10.7).integral.rounded(p: 5), -10.66667) 68 | XCTAssertEqual(CGFloat(-10.8).integral.rounded(p: 5), -10.66667) 69 | XCTAssertEqual(CGFloat(-10.9).integral.rounded(p: 5), -11) 70 | } 71 | 72 | func testRounded() { 73 | XCTAssertEqual(CGFloat(10.0123456789).rounded(p: 0), 10) 74 | XCTAssertEqual(CGFloat(10.0123456789).rounded(p: 1), 10) 75 | XCTAssertEqual(CGFloat(10.0123456789).rounded(p: 2), 10.01) 76 | XCTAssertEqual(CGFloat(10.0123456789).rounded(p: 3), 10.012) 77 | XCTAssertEqual(CGFloat(10.0123456789).rounded(p: 4), 10.0123) 78 | XCTAssertEqual(CGFloat(10.0123456789).rounded(p: 5), 10.01235) 79 | XCTAssertEqual(CGFloat(10.0123456789).rounded(p: 6), 10.012346) 80 | XCTAssertEqual(CGFloat(10.0123456789).rounded(p: 7), 10.0123457) 81 | XCTAssertEqual(CGFloat(10.0123456789).rounded(p: 8), 10.01234568) 82 | XCTAssertEqual(CGFloat(10.0123456789).rounded(p: 9), 10.012345679) 83 | XCTAssertEqual(CGFloat(-10.0123456789).rounded(p: 0), -10) 84 | XCTAssertEqual(CGFloat(-10.0123456789).rounded(p: 1), -10) 85 | XCTAssertEqual(CGFloat(-10.0123456789).rounded(p: 2), -10.01) 86 | XCTAssertEqual(CGFloat(-10.0123456789).rounded(p: 3), -10.012) 87 | XCTAssertEqual(CGFloat(-10.0123456789).rounded(p: 4), -10.0123) 88 | XCTAssertEqual(CGFloat(-10.0123456789).rounded(p: 5), -10.01235) 89 | XCTAssertEqual(CGFloat(-10.0123456789).rounded(p: 6), -10.012346) 90 | XCTAssertEqual(CGFloat(-10.0123456789).rounded(p: 7), -10.0123457) 91 | XCTAssertEqual(CGFloat(-10.0123456789).rounded(p: 8), -10.01234568) 92 | XCTAssertEqual(CGFloat(-10.0123456789).rounded(p: 9), -10.012345679) 93 | } 94 | 95 | func testFloored() { 96 | XCTAssertEqual(CGFloat(10.0123456789).floored(p: 0), 10) 97 | XCTAssertEqual(CGFloat(10.0123456789).floored(p: 1), 10) 98 | XCTAssertEqual(CGFloat(10.0123456789).floored(p: 2), 10.01) 99 | XCTAssertEqual(CGFloat(10.0123456789).floored(p: 3), 10.012) 100 | XCTAssertEqual(CGFloat(10.0123456789).floored(p: 4), 10.0123) 101 | XCTAssertEqual(CGFloat(10.0123456789).floored(p: 5), 10.01234) 102 | XCTAssertEqual(CGFloat(10.0123456789).floored(p: 6), 10.012345) 103 | XCTAssertEqual(CGFloat(10.0123456789).floored(p: 7), 10.0123456) 104 | XCTAssertEqual(CGFloat(10.0123456789).floored(p: 8), 10.01234567) 105 | XCTAssertEqual(CGFloat(10.0123456789).floored(p: 9), 10.012345678) 106 | XCTAssertEqual(CGFloat(-10.0123456789).floored(p: 0), -11) 107 | XCTAssertEqual(CGFloat(-10.0123456789).floored(p: 1), -10.1) 108 | XCTAssertEqual(CGFloat(-10.0123456789).floored(p: 2), -10.02) 109 | XCTAssertEqual(CGFloat(-10.0123456789).floored(p: 3), -10.013) 110 | XCTAssertEqual(CGFloat(-10.0123456789).floored(p: 4), -10.0124) 111 | XCTAssertEqual(CGFloat(-10.0123456789).floored(p: 5), -10.01235) 112 | XCTAssertEqual(CGFloat(-10.0123456789).floored(p: 6), -10.012346) 113 | XCTAssertEqual(CGFloat(-10.0123456789).floored(p: 7), -10.0123457) 114 | XCTAssertEqual(CGFloat(-10.0123456789).floored(p: 8), -10.01234568) 115 | XCTAssertEqual(CGFloat(-10.0123456789).floored(p: 9), -10.012345679) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /GridViewTests/CGRectExtensionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGRectExtensionTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/29. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class CGRectExtensionTests: XCTestCase { 13 | let horizontal = Horizontal(x: 100, width: 100) 14 | let vertical = Vertical(y: 100, height: 100) 15 | 16 | override func setUp() { 17 | super.setUp() 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | } 20 | 21 | override func tearDown() { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | super.tearDown() 24 | } 25 | 26 | func testHorizontalInit() { 27 | let rect = CGRect(horizontal: horizontal) 28 | 29 | XCTAssertEqual(rect.origin.x, 100) 30 | XCTAssertEqual(rect.origin.y, 0) 31 | XCTAssertEqual(rect.size.width, 100) 32 | XCTAssertEqual(rect.size.height, 0) 33 | } 34 | 35 | func testVerticalInit() { 36 | let rect = CGRect(vertical: vertical) 37 | 38 | XCTAssertEqual(rect.origin.x, 0) 39 | XCTAssertEqual(rect.origin.y, 100) 40 | XCTAssertEqual(rect.size.width, 0) 41 | XCTAssertEqual(rect.size.height, 100) 42 | } 43 | 44 | func testInit() { 45 | let rect = CGRect(horizontal: horizontal, vertical: vertical) 46 | 47 | XCTAssertEqual(rect.origin.x, 100) 48 | XCTAssertEqual(rect.origin.y, 100) 49 | XCTAssertEqual(rect.size.width, 100) 50 | XCTAssertEqual(rect.size.height, 100) 51 | } 52 | 53 | func testVerticalProperty() { 54 | var rect = CGRect(x: 100, y: 100, width: 100, height: 100) 55 | 56 | XCTAssertEqual(rect.vertical.y, 100) 57 | XCTAssertEqual(rect.vertical.height, 100) 58 | XCTAssertEqual(rect.vertical.maxY, 200) 59 | 60 | rect.vertical = Vertical(y: 200, height: 200) 61 | 62 | XCTAssertEqual(rect.vertical.y, 200) 63 | XCTAssertEqual(rect.vertical.height, 200) 64 | XCTAssertEqual(rect.vertical.maxY, 400) 65 | } 66 | 67 | func testHorizontalProperty() { 68 | var rect = CGRect(x: 100, y: 100, width: 100, height: 100) 69 | 70 | XCTAssertEqual(rect.horizontal.x, 100) 71 | XCTAssertEqual(rect.horizontal.width, 100) 72 | XCTAssertEqual(rect.horizontal.maxX, 200) 73 | 74 | rect.horizontal = Horizontal(x: 200, width: 200) 75 | 76 | XCTAssertEqual(rect.horizontal.x, 200) 77 | XCTAssertEqual(rect.horizontal.width, 200) 78 | XCTAssertEqual(rect.horizontal.maxX, 400) 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /GridViewTests/CGSizeExtensionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGSizeExtensionTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2017/01/29. 6 | // Copyright © 2017年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class CGSizeExtensionTests: XCTestCase { 13 | override func setUp() { 14 | super.setUp() 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | super.tearDown() 21 | } 22 | 23 | func testOperator() { 24 | let size1 = CGSize(width: 10, height: 20) 25 | let size2 = CGSize(width: -10, height: -20) 26 | 27 | XCTAssertEqual(size1 + size1, CGSize(width: 20, height: 40)) 28 | XCTAssertEqual(size1 + size2, .zero) 29 | XCTAssertEqual(size2 + size1, .zero) 30 | XCTAssertEqual(size2 + size2, CGSize(width: -20, height: -40)) 31 | 32 | XCTAssertEqual(size1 - size1, .zero) 33 | XCTAssertEqual(size1 - size2, CGSize(width: 20, height: 40)) 34 | XCTAssertEqual(size2 - size1, CGSize(width: -20, height: -40)) 35 | XCTAssertEqual(size2 - size2, .zero) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /GridViewTests/CountableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountableTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/29. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class CountableTests: XCTestCase { 13 | let array = [0,1,2,3,4,5,6,7,8,9] 14 | 15 | override func setUp() { 16 | super.setUp() 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | } 19 | 20 | override func tearDown() { 21 | // Put teardown code here. This method is called after the invocation of each test method in the class. 22 | super.tearDown() 23 | } 24 | 25 | func testRepeat() { 26 | XCTAssertEqual(array.repeat(5), 5) 27 | XCTAssertEqual(array.repeat(13), 3) 28 | XCTAssertEqual(array.repeat(20), 0) 29 | XCTAssertEqual(array.repeat(35), 5) 30 | XCTAssertEqual(array.repeat(-3), 7) 31 | XCTAssertEqual(array.repeat(-5), 5) 32 | XCTAssertEqual(array.repeat(-13), 7) 33 | XCTAssertEqual(array.repeat(-20), 0) 34 | XCTAssertEqual(array.repeat(-35), 5) 35 | } 36 | 37 | func testThreshold() { 38 | XCTAssertEqual(array.threshold(with: 5), .in) 39 | XCTAssertEqual(array.threshold(with: 13), .above) 40 | XCTAssertEqual(array.threshold(with: 20), .above) 41 | XCTAssertEqual(array.threshold(with: 35), .above) 42 | XCTAssertEqual(array.threshold(with: -5), .below) 43 | XCTAssertEqual(array.threshold(with: -13), .below) 44 | XCTAssertEqual(array.threshold(with: -20), .below) 45 | XCTAssertEqual(array.threshold(with: -35), .below) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /GridViewTests/GridViewCellTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridViewCellTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/30. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class GridViewCellTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testInit() { 25 | let cell1 = MockCell.nib.instantiate(withOwner: nil, options: nil).first as? GridViewCell 26 | let cell2 = GridViewCell(frame: .zero) 27 | 28 | XCTAssertEqual(cell1?.autoresizingMask, UIView.AutoresizingMask(rawValue: 0)) 29 | XCTAssertEqual(cell2.autoresizingMask, UIView.AutoresizingMask(rawValue: 0)) 30 | 31 | XCTAssertNotNil(GridViewCell().prepareForReuse()) 32 | XCTAssertNotNil(GridViewCell().setSelected(true)) 33 | XCTAssertNotNil(GridViewCell().setSelected(false)) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /GridViewTests/GridViewScrollPositionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridViewScrollPositionTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/29. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class GridViewScrollPositionTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testInit() { 25 | let p1 = GridViewScrollPosition(rawValue: 1) 26 | XCTAssertEqual(p1.rawValue, 1) 27 | XCTAssertEqual(p1, .top) 28 | 29 | let p2 = GridViewScrollPosition(rawValue: 2) 30 | XCTAssertEqual(p2.rawValue, 2) 31 | XCTAssertEqual(p2, .centeredVertically) 32 | 33 | let p3 = GridViewScrollPosition(rawValue: 3) 34 | XCTAssertEqual(p3.rawValue, 3) 35 | XCTAssertEqual(p3, [.top, .centeredVertically]) 36 | 37 | let p4 = GridViewScrollPosition(rawValue: 4) 38 | XCTAssertEqual(p4.rawValue, 4) 39 | XCTAssertEqual(p4, .bottom) 40 | 41 | let p5 = GridViewScrollPosition(rawValue: 5) 42 | XCTAssertEqual(p5.rawValue, 5) 43 | XCTAssertEqual(p5, [.bottom, .top]) 44 | } 45 | 46 | func testContains() { 47 | let positions: [GridViewScrollPosition] = [.top, .centeredVertically, .bottom, .left, .centeredHorizontally, .right, .topFit, .bottomFit, .rightFit, .leftFit] 48 | 49 | XCTAssertTrue(positions.contains(GridViewScrollPosition.top)) 50 | XCTAssertTrue(positions.contains(GridViewScrollPosition.centeredVertically)) 51 | XCTAssertTrue(positions.contains(GridViewScrollPosition.bottom)) 52 | XCTAssertTrue(positions.contains(GridViewScrollPosition.left)) 53 | XCTAssertTrue(positions.contains(GridViewScrollPosition.centeredHorizontally)) 54 | XCTAssertTrue(positions.contains(GridViewScrollPosition.right)) 55 | XCTAssertTrue(positions.contains(GridViewScrollPosition.topFit)) 56 | XCTAssertTrue(positions.contains(GridViewScrollPosition.bottomFit)) 57 | XCTAssertTrue(positions.contains(GridViewScrollPosition.rightFit)) 58 | XCTAssertTrue(positions.contains(GridViewScrollPosition.leftFit)) 59 | XCTAssertFalse(positions.contains(GridViewScrollPosition(rawValue: 1 << 10))) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /GridViewTests/GridViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridViewTests.swift 3 | // GridViewTests 4 | // 5 | // Created by Kyohei Ito on 2016/11/27. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class GridViewTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /GridViewTests/HorizontalTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HorizontalTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/29. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class HorizontalTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testInit() { 25 | let horizontal = Horizontal(x: 100, width: 100) 26 | 27 | XCTAssertEqual(horizontal.x, 100) 28 | XCTAssertEqual(horizontal.width, 100) 29 | XCTAssertEqual(horizontal.maxX, 200) 30 | } 31 | 32 | func testZero() { 33 | XCTAssertEqual(Horizontal.zero.x, 0) 34 | XCTAssertEqual(Horizontal.zero.width, 0) 35 | XCTAssertEqual(Horizontal.zero.maxX, 0) 36 | } 37 | 38 | func testMaxX() { 39 | var horizontal = Horizontal(x: 100, width: 100) 40 | XCTAssertEqual(horizontal.maxX, 200) 41 | 42 | horizontal.x = 200 43 | XCTAssertEqual(horizontal.maxX, 300) 44 | 45 | horizontal.width = 200 46 | XCTAssertEqual(horizontal.maxX, 400) 47 | } 48 | 49 | func testOperator() { 50 | let horizontal = Horizontal(x: 100, width: 100) * 10 51 | 52 | XCTAssertEqual(horizontal.x, 1000) 53 | XCTAssertEqual(horizontal.width, 1000) 54 | XCTAssertEqual(horizontal.maxX, 2000) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /GridViewTests/IndexPathTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IndexPathTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2017/01/21. 6 | // Copyright © 2017年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class IndexPathTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testInit() { 25 | XCTAssertEqual(IndexPath(row: 0, column: 0), IndexPath(row: 0, section: 0)) 26 | XCTAssertEqual(IndexPath(row: 2, column: 2), IndexPath(row: 2, section: 2)) 27 | XCTAssertEqual(IndexPath(row: 5, column: 5), IndexPath(row: 5, section: 5)) 28 | XCTAssertEqual(IndexPath(row: 7, column: 7), IndexPath(row: 7, section: 7)) 29 | XCTAssertEqual(IndexPath(row: 10, column: 10), IndexPath(row: 10, section: 10)) 30 | } 31 | 32 | func testColumn() { 33 | var indexPath = IndexPath(row: 0, section: 0) 34 | 35 | XCTAssertEqual(indexPath.section, 0) 36 | XCTAssertEqual(indexPath.column, 0) 37 | 38 | indexPath.section = 2 39 | XCTAssertEqual(indexPath.section, 2) 40 | XCTAssertEqual(indexPath.column, 2) 41 | 42 | indexPath.column = 5 43 | XCTAssertEqual(indexPath.section, 5) 44 | XCTAssertEqual(indexPath.column, 5) 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /GridViewTests/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 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /GridViewTests/LocationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/29. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class LocationTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testOperator() { 25 | XCTAssertTrue(MockLocation(x: 10, y: 10) == MockLocation(x: 10, y: 10)) 26 | XCTAssertFalse(MockLocation(x: 10, y: 10) == MockLocation(x: 20, y: 10)) 27 | XCTAssertFalse(MockLocation(x: 10, y: 10) == MockLocation(x: 10, y: 20)) 28 | XCTAssertFalse(MockLocation(x: 10, y: 10) == MockLocation(x: 20, y: 20)) 29 | 30 | let l1 = MockLocation(x: 10, y: 10) + MockLocation(x: 10, y: 10) 31 | XCTAssertEqual(l1.x, 20) 32 | XCTAssertEqual(l1.y, 20) 33 | 34 | let l2 = MockLocation(x: 10, y: 10) - MockLocation(x: 10, y: 10) 35 | XCTAssertEqual(l2.x, 0) 36 | XCTAssertEqual(l2.y, 0) 37 | 38 | let l3 = MockLocation(x: 10, y: 10) * MockLocation(x: 10, y: 10) 39 | XCTAssertEqual(l3.x, 100) 40 | XCTAssertEqual(l3.y, 100) 41 | 42 | let l4 = MockLocation(x: 10, y: 10) / MockLocation(x: 10, y: 10) 43 | XCTAssertEqual(l4.x, 1) 44 | XCTAssertEqual(l4.y, 1) 45 | 46 | let l5 = MockLocation(x: 10, y: 10) + 10 47 | XCTAssertEqual(l5.x, 20) 48 | XCTAssertEqual(l5.y, 20) 49 | 50 | let l6 = MockLocation(x: 10, y: 10) - 10 51 | XCTAssertEqual(l6.x, 0) 52 | XCTAssertEqual(l6.y, 0) 53 | } 54 | 55 | func testMin() { 56 | XCTAssertEqual(MockLocation(x: 10, y: 10).min(), 10) 57 | XCTAssertEqual(MockLocation(x: 1, y: 10).min(), 1) 58 | XCTAssertEqual(MockLocation(x: 10, y: 1).min(), 1) 59 | 60 | XCTAssertEqual(min(MockLocation(x: 10, y: 10), MockLocation(x: 10, y: 1), MockLocation(x: 1, y: 10)), MockLocation(x: 1, y: 1)) 61 | XCTAssertEqual(min(MockLocation(x: 10, y: 10), MockLocation(x: 10, y: 1), MockLocation(x: 10, y: 5)), MockLocation(x: 10, y: 1)) 62 | XCTAssertEqual(min(MockLocation(x: 10, y: 10), MockLocation(x: 1, y: 10), MockLocation(x: 5, y: 10)), MockLocation(x: 1, y: 10)) 63 | XCTAssertEqual(min(MockLocation(x: 1, y: 1), MockLocation(x: 5, y: 5), MockLocation(x: 10, y: 10)), MockLocation(x: 1, y: 1)) 64 | XCTAssertEqual(min(MockLocation(x: 10, y: 10)), MockLocation(x: 10, y: 10)) 65 | } 66 | 67 | func testMax() { 68 | XCTAssertEqual(MockLocation(x: 10, y: 10).max(), 10) 69 | XCTAssertEqual(MockLocation(x: 1, y: 10).max(), 10) 70 | XCTAssertEqual(MockLocation(x: 10, y: 1).max(), 10) 71 | 72 | XCTAssertEqual(max(MockLocation(x: 10, y: 10), MockLocation(x: 10, y: 1), MockLocation(x: 1, y: 10)), MockLocation(x: 10, y: 10)) 73 | XCTAssertEqual(max(MockLocation(x: 1, y: 1), MockLocation(x: 10, y: 1), MockLocation(x: 5, y: 1)), MockLocation(x: 10, y: 1)) 74 | XCTAssertEqual(max(MockLocation(x: 1, y: 1), MockLocation(x: 1, y: 10), MockLocation(x: 1, y: 5)), MockLocation(x: 1, y: 10)) 75 | XCTAssertEqual(max(MockLocation(x: 10, y: 10), MockLocation(x: 5, y: 5), MockLocation(x: 1, y: 1)), MockLocation(x: 10, y: 10)) 76 | XCTAssertEqual(max(MockLocation(x: 10, y: 10)), MockLocation(x: 10, y: 10)) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /GridViewTests/Mock/MockCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockCell.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/30. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | @testable import GridView 11 | 12 | class MockCell: GridViewCell { 13 | static var nib: UINib { 14 | return UINib(nibName: "MockCell", bundle: Bundle(for: self)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /GridViewTests/Mock/MockCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /GridViewTests/Mock/MockLocation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockLocation.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/30. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | @testable import GridView 11 | 12 | struct MockLocation: Location { 13 | public var x: CGFloat 14 | public var y: CGFloat 15 | 16 | public init(x: CGFloat, y: CGFloat) { 17 | self.x = x 18 | self.y = y 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /GridViewTests/Mock/MockReusable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockReusable.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/30. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | @testable import GridView 11 | 12 | class MockReusable: Reusable { 13 | var canReuse = true 14 | } 15 | -------------------------------------------------------------------------------- /GridViewTests/NeedsLayoutTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NeedsLayoutTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/29. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class NeedsLayoutTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testNeedsLayoutEquatable() { 25 | let none = NeedsLayout.none 26 | let reload = NeedsLayout.reload 27 | let layout = NeedsLayout.layout(.all(ViewMatrix())) 28 | 29 | XCTAssertTrue(none == none) 30 | XCTAssertTrue(reload == reload) 31 | XCTAssertTrue(layout == layout) 32 | 33 | XCTAssertFalse(none == reload) 34 | XCTAssertFalse(none == layout) 35 | XCTAssertFalse(reload == none) 36 | XCTAssertFalse(reload == layout) 37 | XCTAssertFalse(layout == none) 38 | XCTAssertFalse(layout == reload) 39 | } 40 | 41 | func testNeedsLayoutComparable() { 42 | let none = NeedsLayout.none 43 | let layout = NeedsLayout.layout(.all(ViewMatrix())) 44 | let reload = NeedsLayout.reload 45 | 46 | XCTAssertFalse(none < none) 47 | XCTAssertTrue(none < reload) 48 | XCTAssertTrue(none < layout) 49 | 50 | XCTAssertFalse(layout < none) 51 | XCTAssertTrue(layout < reload) 52 | XCTAssertFalse(layout < layout) 53 | 54 | XCTAssertFalse(reload < none) 55 | XCTAssertFalse(reload < reload) 56 | XCTAssertFalse(reload < layout) 57 | } 58 | 59 | func testLayoutTypeEquatable() { 60 | let matrix = ViewMatrix() 61 | let all = NeedsLayout.LayoutType.all(matrix) 62 | let horizontally = NeedsLayout.LayoutType.horizontally(matrix) 63 | let rotating = NeedsLayout.LayoutType.rotating(matrix) 64 | let scaling = NeedsLayout.LayoutType.scaling(matrix) 65 | let pinching = NeedsLayout.LayoutType.pinching(matrix) 66 | 67 | XCTAssertTrue(all == all) 68 | XCTAssertTrue(horizontally == horizontally) 69 | XCTAssertTrue(rotating == rotating) 70 | XCTAssertTrue(scaling == scaling) 71 | XCTAssertTrue(pinching == pinching) 72 | 73 | XCTAssertFalse(all == horizontally) 74 | XCTAssertFalse(all == rotating) 75 | XCTAssertFalse(all == scaling) 76 | XCTAssertFalse(all == pinching) 77 | XCTAssertFalse(horizontally == all) 78 | XCTAssertFalse(horizontally == rotating) 79 | XCTAssertFalse(horizontally == scaling) 80 | XCTAssertFalse(horizontally == pinching) 81 | XCTAssertFalse(rotating == all) 82 | XCTAssertFalse(rotating == horizontally) 83 | XCTAssertFalse(rotating == scaling) 84 | XCTAssertFalse(rotating == pinching) 85 | XCTAssertFalse(scaling == all) 86 | XCTAssertFalse(scaling == horizontally) 87 | XCTAssertFalse(scaling == rotating) 88 | XCTAssertFalse(scaling == pinching) 89 | XCTAssertFalse(pinching == all) 90 | XCTAssertFalse(pinching == horizontally) 91 | XCTAssertFalse(pinching == scaling) 92 | XCTAssertFalse(pinching == rotating) 93 | } 94 | 95 | func testLayoutTypeComparable() { 96 | let matrix = ViewMatrix() 97 | let all = NeedsLayout.LayoutType.all(matrix) 98 | let horizontally = NeedsLayout.LayoutType.horizontally(matrix) 99 | let rotating = NeedsLayout.LayoutType.rotating(matrix) 100 | let scaling = NeedsLayout.LayoutType.scaling(matrix) 101 | let pinching = NeedsLayout.LayoutType.pinching(matrix) 102 | 103 | XCTAssertFalse(all < all) 104 | XCTAssertFalse(all < horizontally) 105 | XCTAssertFalse(all < rotating) 106 | XCTAssertFalse(all < scaling) 107 | XCTAssertFalse(all < pinching) 108 | 109 | XCTAssertTrue(horizontally < all) 110 | XCTAssertFalse(horizontally < horizontally) 111 | XCTAssertFalse(horizontally < rotating) 112 | XCTAssertFalse(horizontally < scaling) 113 | XCTAssertFalse(horizontally < pinching) 114 | 115 | XCTAssertTrue(rotating < all) 116 | XCTAssertTrue(rotating < horizontally) 117 | XCTAssertFalse(rotating < rotating) 118 | XCTAssertFalse(rotating < scaling) 119 | XCTAssertFalse(rotating < pinching) 120 | 121 | XCTAssertTrue(scaling < all) 122 | XCTAssertTrue(scaling < horizontally) 123 | XCTAssertTrue(scaling < rotating) 124 | XCTAssertFalse(scaling < scaling) 125 | XCTAssertFalse(scaling < pinching) 126 | 127 | XCTAssertTrue(pinching < all) 128 | XCTAssertTrue(pinching < horizontally) 129 | XCTAssertTrue(pinching < rotating) 130 | XCTAssertTrue(pinching < scaling) 131 | XCTAssertFalse(pinching < pinching) 132 | } 133 | 134 | func testLayoutTypeMatrix() { 135 | let matrix = ViewMatrix() 136 | 137 | XCTAssertNotNil(NeedsLayout.LayoutType.all(matrix).matrix) 138 | XCTAssertNotNil(NeedsLayout.LayoutType.horizontally(matrix).matrix) 139 | XCTAssertNotNil(NeedsLayout.LayoutType.rotating(matrix).matrix) 140 | XCTAssertNotNil(NeedsLayout.LayoutType.pinching(matrix).matrix) 141 | } 142 | 143 | func testDebugDescription() { 144 | let matrix = ViewMatrix() 145 | 146 | XCTAssertNotNil(NeedsLayout.none.debugDescription) 147 | XCTAssertNotNil(NeedsLayout.layout(.all(ViewMatrix())).debugDescription) 148 | XCTAssertNotNil(NeedsLayout.reload.debugDescription) 149 | 150 | XCTAssertNotNil(NeedsLayout.LayoutType.all(matrix).debugDescription) 151 | XCTAssertNotNil(NeedsLayout.LayoutType.horizontally(matrix).debugDescription) 152 | XCTAssertNotNil(NeedsLayout.LayoutType.rotating(matrix).debugDescription) 153 | XCTAssertNotNil(NeedsLayout.LayoutType.pinching(matrix).debugDescription) 154 | } 155 | 156 | func testIsScaling() { 157 | let matrix = ViewMatrix() 158 | let all = NeedsLayout.LayoutType.all(matrix) 159 | let horizontally = NeedsLayout.LayoutType.horizontally(matrix) 160 | let rotating = NeedsLayout.LayoutType.rotating(matrix) 161 | let scaling = NeedsLayout.LayoutType.scaling(matrix) 162 | let pinching = NeedsLayout.LayoutType.pinching(matrix) 163 | 164 | XCTAssertFalse(all.isScaling) 165 | XCTAssertFalse(horizontally.isScaling) 166 | XCTAssertFalse(rotating.isScaling) 167 | XCTAssertTrue(scaling.isScaling) 168 | XCTAssertTrue(pinching.isScaling) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /GridViewTests/ReuseQueueTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReuseQueueTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/29. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | private class MockView: UIView, Reusable {} 13 | 14 | class ReuseQueueTests: XCTestCase { 15 | 16 | override func setUp() { 17 | super.setUp() 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | } 20 | 21 | override func tearDown() { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | super.tearDown() 24 | } 25 | 26 | func testExtensionDefaults() { 27 | XCTAssertNotNil(MockReusable().prepareForReuse()) 28 | 29 | let view = MockView() 30 | XCTAssertTrue(view.canReuse) 31 | 32 | let superview = UIView() 33 | superview.addSubview(view) 34 | 35 | XCTAssertFalse(view.canReuse) 36 | } 37 | 38 | func testAppend() { 39 | var queue = ReuseQueue() 40 | 41 | XCTAssertNil(queue.dequeue(with: "1")) 42 | XCTAssertNil(queue.dequeue(with: "2")) 43 | 44 | queue.append(MockReusable(), for: "1") 45 | 46 | XCTAssertNotNil(queue.dequeue(with: "1")) 47 | XCTAssertNil(queue.dequeue(with: "2")) 48 | 49 | queue.append(MockReusable(), for: "2") 50 | 51 | XCTAssertNotNil(queue.dequeue(with: "1")) 52 | XCTAssertNotNil(queue.dequeue(with: "2")) 53 | } 54 | 55 | func testDequeue() { 56 | var queue = ReuseQueue() 57 | 58 | XCTAssertNil(queue.dequeue(with: "1")) 59 | 60 | let reusable = MockReusable() 61 | queue.append(reusable, for: "1") 62 | 63 | XCTAssertNotNil(queue.dequeue(with: "1")) 64 | XCTAssertNotNil(queue.dequeue(with: "1")) 65 | 66 | reusable.canReuse = false 67 | XCTAssertNil(queue.dequeue(with: "1")) 68 | XCTAssertNil(queue.dequeue(with: "1")) 69 | 70 | reusable.canReuse = true 71 | XCTAssertNotNil(queue.dequeue(with: "1")) 72 | 73 | reusable.canReuse = false 74 | XCTAssertNil(queue.dequeue(with: "1")) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /GridViewTests/ScaleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScaleTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/30. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class ScaleTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testInit() { 25 | let scale = Scale(x: 100, y: 100) 26 | 27 | XCTAssertEqual(scale.x, 100) 28 | XCTAssertEqual(scale.y, 100) 29 | } 30 | 31 | func testZero() { 32 | XCTAssertEqual(Scale.zero.x, 0) 33 | XCTAssertEqual(Scale.zero.y, 0) 34 | } 35 | 36 | func testDefault() { 37 | XCTAssertEqual(Scale.default.x, 1) 38 | XCTAssertEqual(Scale.default.y, 1) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /GridViewTests/UIEdgeInsetsExtensionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIEdgeInsetsExtensionTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2017/01/29. 6 | // Copyright © 2017年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class UIEdgeInsetsExtensionTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testOperator() { 25 | let inset1 = UIEdgeInsets(top: 10, left: 20, bottom: 30, right: 40) 26 | let inset2 = UIEdgeInsets(top: -10, left: -20, bottom: -30, right: -40) 27 | 28 | XCTAssertEqual(inset1 + inset1, UIEdgeInsets(top: 20, left: 40, bottom: 60, right: 80)) 29 | XCTAssertEqual(inset1 + inset2, .zero) 30 | XCTAssertEqual(inset2 + inset1, .zero) 31 | XCTAssertEqual(inset2 + inset2, UIEdgeInsets(top: -20, left: -40, bottom: -60, right: -80)) 32 | 33 | XCTAssertEqual(inset1 - inset1, .zero) 34 | XCTAssertEqual(inset1 - inset2, UIEdgeInsets(top: 20, left: 40, bottom: 60, right: 80)) 35 | XCTAssertEqual(inset2 - inset1, UIEdgeInsets(top: -20, left: -40, bottom: -60, right: -80)) 36 | XCTAssertEqual(inset2 - inset2, .zero) 37 | } 38 | 39 | func testHorizontal() { 40 | let inset1 = UIEdgeInsets(top: 10, left: 20, bottom: 30, right: 40) 41 | let inset2 = UIEdgeInsets(top: -10, left: -20, bottom: -30, right: -40) 42 | 43 | XCTAssertEqual(inset1.horizontal, 60) 44 | XCTAssertEqual(inset2.horizontal, -60) 45 | 46 | XCTAssertEqual(inset1.vertical, 40) 47 | XCTAssertEqual(inset2.vertical, -40) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /GridViewTests/UIScrollViewExtensionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollViewExtensionTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/30. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class UIScrollViewExtensionTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testValidityContentOffset() { 25 | let view = UIView(frame: CGRect(origin: .zero, size: CGSize(width: 100, height: 100))) 26 | let scrollView = UIScrollView(frame: CGRect(x: 10, y: 10, width: 80, height: 80)) 27 | view.addSubview(scrollView) 28 | 29 | scrollView.contentSize = view.bounds.size 30 | 31 | XCTAssertEqual(scrollView.validityContentOffset.x, -10) 32 | XCTAssertEqual(scrollView.validityContentOffset.y, -10) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /GridViewTests/VerticalTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VerticalTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/30. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class VerticalTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testInit() { 25 | let vertical = Vertical(y: 100, height: 100) 26 | 27 | XCTAssertEqual(vertical.y, 100) 28 | XCTAssertEqual(vertical.height, 100) 29 | XCTAssertEqual(vertical.maxY, 200) 30 | } 31 | 32 | func testZero() { 33 | XCTAssertEqual(Vertical.zero.y, 0) 34 | XCTAssertEqual(Vertical.zero.height, 0) 35 | XCTAssertEqual(Vertical.zero.maxY, 0) 36 | } 37 | 38 | func testMaxX() { 39 | var vertical = Vertical(y: 100, height: 100) 40 | XCTAssertEqual(vertical.maxY, 200) 41 | 42 | vertical.y = 200 43 | XCTAssertEqual(vertical.maxY, 300) 44 | 45 | vertical.height = 200 46 | XCTAssertEqual(vertical.maxY, 400) 47 | } 48 | 49 | func testOperator() { 50 | let vertical = Vertical(y: 100, height: 100) * 10 51 | 52 | XCTAssertEqual(vertical.y, 1000) 53 | XCTAssertEqual(vertical.height, 1000) 54 | XCTAssertEqual(vertical.maxY, 2000) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /GridViewTests/ViewBundleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewBundleTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/30. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class ViewBundleTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testRegisterOfNib() { 25 | var bundle = ViewBundle() 26 | 27 | bundle.register(ofNib: MockCell.nib, for: "MockCellNib") 28 | XCTAssertNotNil(bundle.instantiate(with: "MockCellNib")) 29 | } 30 | 31 | func testRegisterOfClass() { 32 | var bundle = ViewBundle() 33 | 34 | bundle.register(ofClass: MockCell.self, for: "MockCellClass") 35 | XCTAssertNotNil(bundle.instantiate(with: "MockCellClass")) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /GridViewTests/ViewReferenceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewReferenceTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/30. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class ViewReferenceTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testInit() { 25 | let view = UIView() 26 | let ref = ViewReference(view) 27 | 28 | XCTAssertNotNil(ref.view) 29 | } 30 | 31 | func testDeinit() { 32 | let view = UIView() 33 | let superview = UIView() 34 | superview.addSubview(view) 35 | 36 | XCTAssertNotNil(view.superview) 37 | 38 | var ref: ViewReference? = ViewReference(view) 39 | XCTAssertNotNil(ref?.view?.superview) 40 | XCTAssertNotNil(view.superview) 41 | 42 | ref = nil 43 | XCTAssertNil(ref?.view?.superview) 44 | XCTAssertNil(view.superview) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /GridViewTests/ViewVisibleInfoTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewVisibleInfoTests.swift 3 | // GridView 4 | // 5 | // Created by Kyohei Ito on 2016/12/30. 6 | // Copyright © 2016年 Kyohei Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GridView 11 | 12 | class ViewVisibleInfoTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testcolumns() { 25 | var info = ViewVisibleInfo() 26 | 27 | info.replaceColumn([0,1,2]) 28 | XCTAssertEqual(info.columns(), [0,1,2]) 29 | 30 | info.replaceColumn([3,4,5]) 31 | XCTAssertEqual(info.columns(), [3,4,5]) 32 | } 33 | 34 | func testRows() { 35 | var info = ViewVisibleInfo() 36 | 37 | info.replaceColumn([0,1,2]) 38 | info.replaceRows { column -> [Int] in 39 | [0,1,2] 40 | } 41 | XCTAssertEqual(info.columns(), [0,1,2]) 42 | XCTAssertEqual(info.rows()[0]!, [0,1,2]) 43 | XCTAssertEqual(info.rows()[1]!, [0,1,2]) 44 | XCTAssertEqual(info.rows()[2]!, [0,1,2]) 45 | XCTAssertEqual(info.rows(in: 0), [0,1,2]) 46 | XCTAssertEqual(info.rows(in: 1), [0,1,2]) 47 | XCTAssertEqual(info.rows(in: 2), [0,1,2]) 48 | XCTAssertEqual(info.rows(in: 3), []) 49 | 50 | info.replaceColumn([3,4,5]) 51 | info.replaceRows { column -> [Int] in 52 | [3,4,5] 53 | } 54 | XCTAssertEqual(info.columns(), [3,4,5]) 55 | XCTAssertEqual(info.rows()[3]!, [3,4,5]) 56 | XCTAssertEqual(info.rows()[4]!, [3,4,5]) 57 | XCTAssertEqual(info.rows()[5]!, [3,4,5]) 58 | XCTAssertEqual(info.rows(in: 3), [3,4,5]) 59 | XCTAssertEqual(info.rows(in: 4), [3,4,5]) 60 | XCTAssertEqual(info.rows(in: 5), [3,4,5]) 61 | XCTAssertEqual(info.rows(in: 6), []) 62 | } 63 | 64 | func testObjects() { 65 | let view1 = UIView(), view2 = UIView(), view3 = UIView() 66 | let path1 = IndexPath(row: 1, column: 0), path2 = IndexPath(row: 2, column: 0), path3 = IndexPath(row: 3, column: 0) 67 | 68 | var info1 = ViewVisibleInfo() 69 | info1.append(view1, at: path1) 70 | info1.append(view2, at: path2) 71 | info1.append(view3, at: path3) 72 | 73 | XCTAssertEqual(info1.visibleObject()[path1]?.view, view1) 74 | XCTAssertEqual(info1.visibleObject()[path2]?.view, view2) 75 | XCTAssertEqual(info1.visibleObject()[path3]?.view, view3) 76 | XCTAssertEqual(info1.object(at: path1), view1) 77 | XCTAssertEqual(info1.object(at: path2), view2) 78 | XCTAssertEqual(info1.object(at: path3), view3) 79 | 80 | let view4 = UIView(), view5 = UIView(), view6 = UIView() 81 | let path4 = IndexPath(row: 4, column: 0), path5 = IndexPath(row: 5, column: 0), path6 = IndexPath(row: 6, column: 0) 82 | var info2 = ViewVisibleInfo() 83 | info2.append(view4, at: path4) 84 | info2.append(view5, at: path5) 85 | info2.append(view6, at: path6) 86 | 87 | XCTAssertEqual(info2.visibleObject()[path4]?.view, view4) 88 | XCTAssertEqual(info2.visibleObject()[path5]?.view, view5) 89 | XCTAssertEqual(info2.visibleObject()[path6]?.view, view6) 90 | XCTAssertEqual(info2.object(at: path4), view4) 91 | XCTAssertEqual(info2.object(at: path5), view5) 92 | XCTAssertEqual(info2.object(at: path6), view6) 93 | 94 | info1.replaceObject(with: info2) 95 | XCTAssertNil(info1.visibleObject()[path1]?.view) 96 | XCTAssertNil(info1.visibleObject()[path2]?.view) 97 | XCTAssertNil(info1.visibleObject()[path3]?.view) 98 | XCTAssertNil(info1.object(at: path1)) 99 | XCTAssertNil(info1.object(at: path2)) 100 | XCTAssertNil(info1.object(at: path3)) 101 | 102 | XCTAssertNotNil(info1.visibleObject()[path4]?.view) 103 | XCTAssertNotNil(info1.visibleObject()[path5]?.view) 104 | XCTAssertNotNil(info1.visibleObject()[path6]?.view) 105 | XCTAssertNotNil(info1.object(at: path4)) 106 | XCTAssertNotNil(info1.object(at: path5)) 107 | XCTAssertNotNil(info1.object(at: path6)) 108 | 109 | XCTAssertNil(info1.removedObject(at: path1)) 110 | XCTAssertNil(info1.removedObject(at: path2)) 111 | XCTAssertNil(info1.removedObject(at: path3)) 112 | XCTAssertNotNil(info1.removedObject(at: path4)) 113 | XCTAssertNotNil(info1.removedObject(at: path5)) 114 | XCTAssertNotNil(info1.removedObject(at: path6)) 115 | 116 | XCTAssertNil(info1.object(at: path4)) 117 | XCTAssertNil(info1.object(at: path5)) 118 | XCTAssertNil(info1.object(at: path6)) 119 | } 120 | 121 | func testSelected() { 122 | let view1 = UIView() 123 | let path1 = IndexPath(row: 1, column: 0), path2 = IndexPath(row: 2, column: 0), path3 = IndexPath(row: 3, column: 0) 124 | 125 | var info1 = ViewVisibleInfo() 126 | info1.append(view1, at: path1) 127 | 128 | XCTAssertNotNil(info1.selected(at: path1)) 129 | XCTAssertNil(info1.selected(at: path2)) 130 | 131 | XCTAssertTrue(info1.isSelected(path1)) 132 | XCTAssertTrue(info1.isSelected(path2)) 133 | XCTAssertFalse(info1.isSelected(path3)) 134 | 135 | XCTAssertEqual(info1.indexPathsForSelected(), [path1,path2]) 136 | XCTAssertNil(info1.selected(at: path3)) 137 | XCTAssertNotNil(info1.deselected(at: path1)) 138 | XCTAssertEqual(info1.indexPathsForSelected(), [path2,path3]) 139 | XCTAssertNil(info1.deselected(at: path2)) 140 | XCTAssertEqual(info1.indexPathsForSelected(), [path3]) 141 | 142 | let view4 = UIView(), view5 = UIView(), view6 = UIView() 143 | let path4 = IndexPath(row: 4, column: 0), path5 = IndexPath(row: 5, column: 0), path6 = IndexPath(row: 6, column: 0) 144 | var info2 = ViewVisibleInfo() 145 | info2.append(view4, at: path4) 146 | info2.append(view5, at: path5) 147 | info2.append(view6, at: path6) 148 | XCTAssertNotNil(info2.selected(at: path4)) 149 | XCTAssertNotNil(info2.selected(at: path5)) 150 | XCTAssertNotNil(info2.selected(at: path6)) 151 | 152 | XCTAssertEqual(info2.indexPathsForSelected(), [path4,path5,path6]) 153 | info2.replaceSelectedIndexPath(with: info1) 154 | XCTAssertEqual(info1.indexPathsForSelected(), [path3]) 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Kyohei Ito 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "GridView", 8 | platforms: [.iOS(.v9)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, and make them visible to other packages. 11 | .library( 12 | name: "GridView", 13 | targets: ["GridView"]), 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | // .package(url: /* package url */, from: "1.0.0"), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 22 | .target( 23 | name: "GridView", 24 | dependencies: [], path: "GridView"), 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GridView 2 | 3 | [![Build Status](https://travis-ci.org/KyoheiG3/GridView.svg?branch=master)](https://travis-ci.org/KyoheiG3/GridView) 4 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 5 | [![Version](https://img.shields.io/cocoapods/v/G3GridView.svg?style=flat)](http://cocoadocs.org/docsets/G3GridView) 6 | [![License](https://img.shields.io/cocoapods/l/G3GridView.svg?style=flat)](http://cocoadocs.org/docsets/G3GridView) 7 | [![Platform](https://img.shields.io/cocoapods/p/G3GridView.svg?style=flat)](http://cocoadocs.org/docsets/G3GridView) 8 | 9 | GridView can tile the view while reusing it. It has an API like UIKit that works fast. Even when device rotates it smoothly relayout. 10 | 11 | timetable timetable 12 | 13 | #### [Appetize's Demo](https://appetize.io/app/d5qk2927a8y07armrbbdck64c4) 14 | 15 | ### You Can 16 | 17 | - Scroll like paging 18 | - Scroll infinitely 19 | - Scale the view 20 | - Call API like the `UITableView` 21 | 22 | ## Requirements 23 | 24 | - Swift 5.0 25 | - iOS 9.0 or later 26 | 27 | ## How to Install 28 | 29 | #### CocoaPods 30 | 31 | Add the following to your `Podfile`: 32 | 33 | ```Ruby 34 | pod "G3GridView" 35 | ``` 36 | 37 | > :warning: **WARNING :** If you want to install from `CocoaPods`, must add `G3GridView` to Podfile because there is a `GridView` different from this `GridView`. 38 | 39 | #### Carthage 40 | 41 | Add the following to your `Cartfile`: 42 | 43 | ```Ruby 44 | github "KyoheiG3/GridView" 45 | ``` 46 | 47 | ## Over View 48 | 49 | GridView can scroll in any direction while reusing Cell like `UITableView`. Also it is based `UIScrollView` and paging and scaling are possible. If necessary, it is possible to repeat the left and right scroll infinitely. 50 | 51 | GridView is one `UIScrollView`, but the range which Cell is viewed depends on Superview. Cell reuse is also done within the range which Superview is viewed, so its size is very important. 52 | 53 | On the other hand, scaling and paging depend to position and size of GridView. 'bounds' is important for paging, 'frame' is important in scaling. The same is true for offset of content. 54 | 55 | The following image is a visual explanation of the view hierarchy. 56 | 57 | ![Hierarchy](https://github.com/KyoheiG3/assets/blob/master/GridView/hierarchy.png) 58 | 59 | You can use it like the `UITableView` APIs. However, there is concept of `Column`. The following functions are delegate APIs of 'GridView'. 60 | 61 | ```swift 62 | func gridView(_ gridView: GridView, numberOfRowsInColumn column: Int) -> Int 63 | func gridView(_ gridView: GridView, cellForRowAt indexPath: IndexPath) -> GridViewCell 64 | 65 | @objc optional func numberOfColumns(in gridView: GridView) -> Int 66 | ``` 67 | 68 | You can see that must return the count. 69 | 70 | ## Examples 71 | 72 | This project is including two examples that is timetable and paging. Those can change on Interface Builder for following: 73 | 74 | ![Example](https://github.com/KyoheiG3/assets/blob/master/GridView/example.gif) 75 | 76 | Try the two examples. 77 | 78 | | timetable | paging | 79 | |-|-| 80 | |timetable|paging| 81 | 82 | 83 | ## Usage 84 | 85 | ### Variables 86 | 87 | #### Infinite Loop 88 | 89 | A horizontal loop is possible. 90 | 91 | ```swift 92 | open var isInfinitable: Bool 93 | ``` 94 | 95 | - Default is `true`. 96 | - Set `false` if you don't need to loop of view. 97 | 98 | loop 99 | 100 | ```swift 101 | gridView.isInfinitable = true 102 | ``` 103 | 104 | #### Scaling 105 | 106 | Content is done relayout rather than scaling like 'UIScrollView'. 107 | 108 | ```swift 109 | open var minimumScale: Scale 110 | open var maximumScale: Scale 111 | ``` 112 | 113 | - Default for x and y are 1. 114 | - Set the vertical and horizontal scales. 115 | 116 | ```swift 117 | public var currentScale: Scale { get } 118 | ``` 119 | 120 | - Get current vertical and horizontal scales. 121 | 122 | scaling 123 | 124 | ```swift 125 | gridView.minimumScale = Scale(x: 0.5, y: 0.5) 126 | gridView.maximumScale = Scale(x: 1.5, y: 1.5) 127 | ``` 128 | 129 | #### Fill for Cell 130 | 131 | It is possible to decide the placement of Cell at relayout. 132 | 133 | ```swift 134 | open var layoutWithoutFillForCell: Bool 135 | ``` 136 | 137 | - Default is `false`. 138 | - Set `true` if need to improved view layout performance. 139 | 140 | | false | true | 141 | |-|-| 142 | |false|true| 143 | 144 | ```swift 145 | gridView.layoutWithoutFillForCell = true 146 | ``` 147 | 148 | #### Content Offset 149 | 150 | If `isInfinitable` is true, `contentOffset` depends on the content size including size to loop. It is possible to take content offset that actually visible. 151 | 152 | ```swift 153 | open var actualContentOffset: CGPoint { get } 154 | ``` 155 | 156 | #### Delegate 157 | 158 | Set the delegate destination. This delegate property is `UIScrollViewDelegate` but, actually set the `GridViewDelegate`. 159 | 160 | ``` 161 | weak open var dataSource: GridViewDataSource? 162 | open var delegate: UIScrollViewDelegate? 163 | ``` 164 | 165 | ### Functions 166 | 167 | #### State 168 | 169 | Get the view state. 170 | 171 | ```swift 172 | public func visibleCells() -> [T] 173 | public func cellForRow(at indexPath: IndexPath) -> GridViewCell? 174 | public func rectForRow(at indexPath: IndexPath) -> CGRect 175 | public func indexPathsForSelectedRows() -> [IndexPath] 176 | public func indexPathForRow(at position: CGPoint) -> IndexPath 177 | ``` 178 | 179 | #### Operation 180 | 181 | Operate the view. 182 | 183 | ```swift 184 | public func contentScale(_ scale: CGFloat) 185 | public func reloadData() 186 | public func invalidateContentSize() 187 | public func invalidateLayout(horizontally: Bool = default) 188 | public func deselectRow(at indexPath: IndexPath) 189 | override open func setContentOffset(_ contentOffset: CGPoint, animated: Bool) 190 | public func scrollToRow(at indexPath: IndexPath, at scrollPosition: GridViewScrollPosition = default, animated: Bool = default) 191 | ``` 192 | 193 | ## LICENSE 194 | 195 | Under the MIT license. See [LICENSE](./LICENSE) file for details. 196 | --------------------------------------------------------------------------------