Guest User

Untitled

a guest
Jul 21st, 2018
210
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 19.61 KB | None | 0 0
  1. //
  2. // AlignedCollectionViewFlowLayout.swift
  3. //
  4. // Created by Mischa Hildebrand on 12/04/2017.
  5. // Copyright Β© 2017 Mischa Hildebrand.
  6. //
  7. // Licensed under the terms of the MIT license:
  8. //
  9. // Permission is hereby granted, free of charge, to any person obtaining a copy
  10. // of this software and associated documentation files (the "Software"), to deal
  11. // in the Software without restriction, including without limitation the rights
  12. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. // copies of the Software, and to permit persons to whom the Software is
  14. // furnished to do so, subject to the following conditions:
  15. //
  16. // The above copyright notice and this permission notice shall be included in
  17. // all copies or substantial portions of the Software.
  18. //
  19. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. // THE SOFTWARE.
  26. //
  27. import UIKit
  28. // MARK: - πŸ¦† Type definitions
  29. /// An abstract protocol that defines an alignment.
  30. protocol Alignment {}
  31.  
  32. /// Defines an alignment for UI elements.
  33. public enum HorizontalAlignment: Alignment {
  34. case left
  35. case justified
  36. case right
  37. }
  38.  
  39. /// Defines a vertical alignment for UI elements.
  40. public enum VerticalAlignment: Alignment {
  41. case top
  42. case center
  43. case bottom
  44. }
  45.  
  46. /// Describes an axis with respect to which items can be aligned.
  47. private struct AlignmentAxis<A: Alignment> {
  48.  
  49. /// Determines how items are aligned relative to the axis.
  50. let alignment: A
  51.  
  52. /// Defines the position of the axis.
  53. /// * If the `Alignment` is horizontal, the alignment axis is vertical and this is the position on the `x` axis.
  54. /// * If the `Alignment` is vertical, the alignment axis is horizontal and this is the position on the `y` axis.
  55. let position: CGFloat
  56. }
  57.  
  58.  
  59. /// A `UICollectionViewFlowLayout` subclass that gives you control
  60. /// over the horizontal and vertical alignment of the cells.
  61. /// You can use it to align the cells like words in a left- or right-aligned text
  62. /// and you can specify how the cells are vertically aligned in their row.
  63. public class AlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
  64.  
  65. // MARK: - πŸ”Ά Properties
  66.  
  67. /// Determines how the cells are horizontally aligned in a row.
  68. /// - Note: The default is `.justified`.
  69. public var horizontalAlignment: HorizontalAlignment = .justified
  70.  
  71. /// Determines how the cells are vertically aligned in a row.
  72. /// - Note: The default is `.center`.
  73. public var verticalAlignment: VerticalAlignment = .center
  74.  
  75. /// The vertical axis with respect to which the cells are horizontally aligned.
  76. /// For a `justified` alignment the alignment axis is not defined and this value is `nil`.
  77. fileprivate var alignmentAxis: AlignmentAxis<HorizontalAlignment>? {
  78. switch horizontalAlignment {
  79. case .left:
  80. return AlignmentAxis(alignment: HorizontalAlignment.left, position: sectionInset.left)
  81. case .right:
  82. guard let collectionViewWidth = collectionView?.frame.size.width else {
  83. return nil
  84. }
  85. return AlignmentAxis(alignment: HorizontalAlignment.right, position: collectionViewWidth - sectionInset.right)
  86. default:
  87. return nil
  88. }
  89. }
  90.  
  91. /// The width of the area inside the collection view that can be filled with cells.
  92. private var contentWidth: CGFloat? {
  93. guard let collectionViewWidth = collectionView?.frame.size.width else {
  94. return nil
  95. }
  96. return collectionViewWidth - sectionInset.left - sectionInset.right
  97. }
  98.  
  99.  
  100. // MARK: - πŸ‘Ά Initialization
  101.  
  102. /// The designated initializer.
  103. ///
  104. /// - Parameters:
  105. /// - horizontalAlignment: Specifies how the cells are horizontally aligned in a row. --
  106. /// (Default: `.justified`)
  107. /// - verticalAlignment: Specified how the cells are vertically aligned in a row. --
  108. /// (Default: `.center`)
  109. public init(horizontalAlignment: HorizontalAlignment = .justified, verticalAlignment: VerticalAlignment = .center) {
  110. super.init()
  111. self.horizontalAlignment = horizontalAlignment
  112. self.verticalAlignment = verticalAlignment
  113. }
  114.  
  115. required public init?(coder aDecoder: NSCoder) {
  116. super.init(coder: aDecoder)
  117. }
  118.  
  119.  
  120. // MARK: - πŸ…ΎοΈ Overrides
  121.  
  122. override public func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
  123.  
  124. // πŸ’‘ IDEA:
  125. // The approach for computing a cell's frame is to create a rectangle that covers the current line.
  126. // Then we check if the preceding cell's frame intersects with this rectangle.
  127. // If it does, the current item is not the first item in the line. Otherwise it is.
  128. // (Vice-versa for right-aligned cells.)
  129. //
  130. // +---------+----------------------------------------------------------------+---------+
  131. // | | | |
  132. // | | +------------+ | |
  133. // | | | | | |
  134. // | section |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| section |
  135. // | inset | |intersection| | | line rect | inset |
  136. // | |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| |
  137. // | (left) | | | current item | (right) |
  138. // | | +------------+ | |
  139. // | | previous item | |
  140. // +---------+----------------------------------------------------------------+---------+
  141. //
  142. // ℹ️ We need this rather complicated approach because the first item in a line
  143. // is not always left-aligned and the last item in a line is not always right-aligned:
  144. // If there is only one item in a line UICollectionViewFlowLayout will center it.
  145.  
  146. // We may not change the original layout attributes or UICollectionViewFlowLayout might complain.
  147. guard let layoutAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes else {
  148. return nil
  149. }
  150.  
  151. // For a justified layout there's nothing to do here
  152. // as UICollectionViewFlowLayout justifies the items in a line by default.
  153. if horizontalAlignment != .justified {
  154. layoutAttributes.alignHorizontally(collectionViewLayout: self)
  155. }
  156.  
  157. // For a vertically centered layout there's nothing to do here
  158. // as UICollectionViewFlowLayout center-aligns the items in a line by default.
  159. if verticalAlignment != .center {
  160. layoutAttributes.alignVertically(collectionViewLayout: self)
  161. }
  162.  
  163. return layoutAttributes
  164. }
  165.  
  166. override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
  167. // We may not change the original layout attributes or UICollectionViewFlowLayout might complain.
  168. let layoutAttributesObjects = copy(super.layoutAttributesForElements(in: rect))
  169. layoutAttributesObjects?.forEach({ (layoutAttributes) in
  170. setFrame(forLayoutAttributes: layoutAttributes)
  171. })
  172. return layoutAttributesObjects
  173. }
  174.  
  175.  
  176. // MARK: - πŸ‘· Private layout helpers
  177.  
  178. /// Sets the frame for the passed layout attributes object by calling the `layoutAttributesForItem(at:)` function.
  179. private func setFrame(forLayoutAttributes layoutAttributes: UICollectionViewLayoutAttributes) {
  180. if layoutAttributes.representedElementCategory == .cell { // Do not modify header views etc.
  181. let indexPath = layoutAttributes.indexPath
  182. if let newFrame = layoutAttributesForItem(at: indexPath)?.frame {
  183. layoutAttributes.frame = newFrame
  184. }
  185. }
  186. }
  187.  
  188. /// A function to access the `super` implementation of `layoutAttributesForItem(at:)` externally.
  189. ///
  190. /// - Parameter indexPath: The index path of the item for which to return the layout attributes.
  191. /// - Returns: The unmodified layout attributes for the item at the specified index path
  192. /// as computed by `UICollectionViewFlowLayout`.
  193. fileprivate func originalLayoutAttribute(forItemAt indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
  194. return super.layoutAttributesForItem(at: indexPath)
  195. }
  196.  
  197. /// Determines if the `firstItemAttributes`' frame is in the same line
  198. /// as the `secondItemAttributes`' frame.
  199. ///
  200. /// - Parameters:
  201. /// - firstItemAttributes: The first layout attributes object to be compared.
  202. /// - secondItemAttributes: The second layout attributes object to be compared.
  203. /// - Returns: `true` if the frames of the two layout attributes are in the same line, else `false`.
  204. /// `false` is also returned when the layout's `collectionView` property is `nil`.
  205. fileprivate func isFrame(for firstItemAttributes: UICollectionViewLayoutAttributes, inSameLineAsFrameFor secondItemAttributes: UICollectionViewLayoutAttributes) -> Bool {
  206. guard let lineWidth = contentWidth else {
  207. return false
  208. }
  209. let firstItemFrame = firstItemAttributes.frame
  210. let lineFrame = CGRect(x: sectionInset.left,
  211. y: firstItemFrame.origin.y,
  212. width: lineWidth,
  213. height: firstItemFrame.size.height)
  214. return lineFrame.intersects(secondItemAttributes.frame)
  215. }
  216.  
  217. /// Determines the layout attributes objects for all items displayed in the same line as the item
  218. /// represented by the passed `layoutAttributes` object.
  219. ///
  220. /// - Parameter layoutAttributes: The layout attributed that represents the reference item.
  221. /// - Returns: The layout attributes objects representing all other items in the same line.
  222. /// The passed `layoutAttributes` object itself is always contained in the returned array.
  223. fileprivate func layoutAttributes(forItemsInLineWith layoutAttributes: UICollectionViewLayoutAttributes) -> [UICollectionViewLayoutAttributes] {
  224. guard let lineWidth = contentWidth else {
  225. return [layoutAttributes]
  226. }
  227. var lineFrame = layoutAttributes.frame
  228. lineFrame.origin.x = sectionInset.left
  229. lineFrame.size.width = lineWidth
  230. return super.layoutAttributesForElements(in: lineFrame) ?? []
  231. }
  232.  
  233. /// Copmutes the alignment axis with which to align the items represented by the `layoutAttributes` objects vertically.
  234. ///
  235. /// - Parameter layoutAttributes: The layout attributes objects to be vertically aligned.
  236. /// - Returns: The axis with respect to which the layout attributes can be aligned
  237. /// or `nil` if the `layoutAttributes` array is empty.
  238. private func verticalAlignmentAxisForLine(with layoutAttributes: [UICollectionViewLayoutAttributes]) -> AlignmentAxis<VerticalAlignment>? {
  239.  
  240. guard let firstAttribute = layoutAttributes.first else {
  241. return nil
  242. }
  243.  
  244. switch verticalAlignment {
  245. case .top:
  246. let minY = layoutAttributes.reduce(CGFloat.greatestFiniteMagnitude) { min($0, $1.frame.minY) }
  247. return AlignmentAxis(alignment: .top, position: minY)
  248.  
  249. case .bottom:
  250. let maxY = layoutAttributes.reduce(0) { max($0, $1.frame.maxY) }
  251. return AlignmentAxis(alignment: .bottom, position: maxY)
  252.  
  253. default:
  254. let centerY = firstAttribute.center.y
  255. return AlignmentAxis(alignment: .center, position: centerY)
  256. }
  257. }
  258.  
  259. /// Computes the axis with which to align the item represented by the `currentLayoutAttributes` vertically.
  260. ///
  261. /// - Parameter currentLayoutAttributes: The layout attributes representing the item to be vertically aligned.
  262. /// - Returns: The axis with respect to which the item can be aligned.
  263. fileprivate func verticalAlignmentAxis(for currentLayoutAttributes: UICollectionViewLayoutAttributes) -> AlignmentAxis<VerticalAlignment> {
  264. let layoutAttributesInLine = layoutAttributes(forItemsInLineWith: currentLayoutAttributes)
  265. // It's okay to force-unwrap here because we pass a non-empty array.
  266. return verticalAlignmentAxisForLine(with: layoutAttributesInLine)!
  267. }
  268.  
  269. /// Creates a deep copy of the passed array by copying all its items.
  270. ///
  271. /// - Parameter layoutAttributesArray: The array to be copied.
  272. /// - Returns: A deep copy of the passed array.
  273. private func copy(_ layoutAttributesArray: [UICollectionViewLayoutAttributes]?) -> [UICollectionViewLayoutAttributes]? {
  274. return layoutAttributesArray?.map{ $0.copy() } as? [UICollectionViewLayoutAttributes]
  275. }
  276.  
  277. }
  278.  
  279.  
  280. // MARK: - πŸ‘· Layout attributes helpers
  281. fileprivate extension UICollectionViewLayoutAttributes {
  282.  
  283. private var currentSection: Int {
  284. return indexPath.section
  285. }
  286.  
  287. private var currentItem: Int {
  288. return indexPath.item
  289. }
  290.  
  291. /// The index path for the item preceding the item represented by this layout attributes object.
  292. private var precedingIndexPath: IndexPath {
  293. return IndexPath(item: currentItem - 1, section: currentSection)
  294. }
  295.  
  296. /// The index path for the item following the item represented by this layout attributes object.
  297. private var followingIndexPath: IndexPath {
  298. return IndexPath(item: currentItem + 1, section: currentSection)
  299. }
  300.  
  301. /// Checks if the item represetend by this layout attributes object is the first item in the line.
  302. ///
  303. /// - Parameter collectionViewLayout: The layout for which to perform the check.
  304. /// - Returns: `true` if the represented item is the first item in the line, else `false`.
  305. func isRepresentingFirstItemInLine(collectionViewLayout: AlignedCollectionViewFlowLayout) -> Bool {
  306. if currentItem <= 0 {
  307. return true
  308. }
  309. else {
  310. if let layoutAttributesForPrecedingItem = collectionViewLayout.originalLayoutAttribute(forItemAt: precedingIndexPath) {
  311. return !collectionViewLayout.isFrame(for: self, inSameLineAsFrameFor: layoutAttributesForPrecedingItem)
  312. }
  313. else {
  314. return true
  315. }
  316. }
  317. }
  318.  
  319. /// Checks if the item represetend by this layout attributes object is the last item in the line.
  320. ///
  321. /// - Parameter collectionViewLayout: The layout for which to perform the check.
  322. /// - Returns: `true` if the represented item is the last item in the line, else `false`.
  323. func isRepresentingLastItemInLine(collectionViewLayout: AlignedCollectionViewFlowLayout) -> Bool {
  324. guard let itemCount = collectionViewLayout.collectionView?.numberOfItems(inSection: currentSection) else {
  325. return false
  326. }
  327.  
  328. if currentItem >= itemCount - 1 {
  329. return true
  330. }
  331. else {
  332. if let layoutAttributesForFollowingItem = collectionViewLayout.originalLayoutAttribute(forItemAt: followingIndexPath) {
  333. return !collectionViewLayout.isFrame(for: self, inSameLineAsFrameFor: layoutAttributesForFollowingItem)
  334. }
  335. else {
  336. return true
  337. }
  338. }
  339. }
  340.  
  341. /// Moves the layout attributes object's frame so that it is aligned horizontally with the alignment axis.
  342. func align(toAlignmentAxis alignmentAxis: AlignmentAxis<HorizontalAlignment>) {
  343. switch alignmentAxis.alignment {
  344. case .left:
  345. frame.origin.x = alignmentAxis.position
  346. case .right:
  347. frame.origin.x = alignmentAxis.position - frame.size.width
  348. default:
  349. break
  350. }
  351. }
  352.  
  353. /// Moves the layout attributes object's frame so that it is aligned vertically with the alignment axis.
  354. func align(toAlignmentAxis alignmentAxis: AlignmentAxis<VerticalAlignment>) {
  355. switch alignmentAxis.alignment {
  356. case .top:
  357. frame.origin.y = alignmentAxis.position
  358. case .bottom:
  359. frame.origin.y = alignmentAxis.position - frame.size.height
  360. default:
  361. center.y = alignmentAxis.position
  362. }
  363. }
  364.  
  365. /// Positions the frame right of the preceding item's frame, leaving a spacing between the frames
  366. /// as defined by the collection view layout's `minimumInteritemSpacing`.
  367. ///
  368. /// - Parameter collectionViewLayout: The layout on which to perfom the calculations.
  369. private func alignToPrecedingItem(collectionViewLayout: AlignedCollectionViewFlowLayout) {
  370. let itemSpacing = collectionViewLayout.minimumInteritemSpacing
  371.  
  372. if let precedingItemAttributes = collectionViewLayout.layoutAttributesForItem(at: precedingIndexPath) {
  373. frame.origin.x = precedingItemAttributes.frame.maxX + itemSpacing
  374. }
  375. }
  376.  
  377. /// Positions the frame left of the following item's frame, leaving a spacing between the frames
  378. /// as defined by the collection view layout's `minimumInteritemSpacing`.
  379. ///
  380. /// - Parameter collectionViewLayout: The layout on which to perfom the calculations.
  381. private func alignToFollowingItem(collectionViewLayout: AlignedCollectionViewFlowLayout) {
  382. let itemSpacing = collectionViewLayout.minimumInteritemSpacing
  383.  
  384. if let followingItemAttributes = collectionViewLayout.layoutAttributesForItem(at: followingIndexPath) {
  385. frame.origin.x = followingItemAttributes.frame.minX - itemSpacing - frame.size.width
  386. }
  387. }
  388.  
  389. /// Aligns the frame horizontally as specified by the collection view layout's `horizontalAlignment`.
  390. ///
  391. /// - Parameters:
  392. /// - collectionViewLayout: The layout providing the alignment information.
  393. func alignHorizontally(collectionViewLayout: AlignedCollectionViewFlowLayout) {
  394.  
  395. guard let alignmentAxis = collectionViewLayout.alignmentAxis else {
  396. return
  397. }
  398.  
  399. switch collectionViewLayout.horizontalAlignment {
  400.  
  401. case .left:
  402. if isRepresentingFirstItemInLine(collectionViewLayout: collectionViewLayout) {
  403. align(toAlignmentAxis: alignmentAxis)
  404. } else {
  405. alignToPrecedingItem(collectionViewLayout: collectionViewLayout)
  406. }
  407.  
  408. case .right:
  409. if isRepresentingLastItemInLine(collectionViewLayout: collectionViewLayout) {
  410. align(toAlignmentAxis: alignmentAxis)
  411. } else {
  412. alignToFollowingItem(collectionViewLayout: collectionViewLayout)
  413. }
  414.  
  415. default:
  416. return
  417. }
  418. }
  419.  
  420. /// Aligns the frame vertically as specified by the collection view layout's `verticalAlignment`.
  421. ///
  422. /// - Parameter collectionViewLayout: The layout providing the alignment information.
  423. func alignVertically(collectionViewLayout: AlignedCollectionViewFlowLayout) {
  424. let alignmentAxis = collectionViewLayout.verticalAlignmentAxis(for: self)
  425. align(toAlignmentAxis: alignmentAxis)
  426. }
  427.  
  428. }
Add Comment
Please, Sign In to add comment