├── .gitignore ├── Images ├── r0.jpg ├── r1.jpg ├── r2.jpg ├── r3.jpg ├── r4.jpg ├── r5.jpg ├── r6.jpg ├── r7.jpg └── r8.jpg ├── JMCFlexibleDatasource.swift ├── JMCFlexibleLayout.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── januszchudzynski.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── JMCFlexibleLayout ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── JMCFlexibleLayout.swift └── ViewController.swift ├── README.md ├── iPadGif.gif ├── iPadRetina.gif ├── iPadScreenshot.png ├── iPhoneScreenshot.png └── iPhoneScreenshot2.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | # CocoaPods 31 | # 32 | # We recommend against adding the Pods directory to your .gitignore. However 33 | # you should judge for yourself, the pros and cons are mentioned at: 34 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 35 | # 36 | # Pods/ 37 | 38 | # Carthage 39 | # 40 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 41 | # Carthage/Checkouts 42 | 43 | Carthage/Build 44 | 45 | # fastlane 46 | # 47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 48 | # screenshots whenever they are needed. 49 | # For more information about the recommended setup visit: 50 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 51 | 52 | fastlane/report.xml 53 | fastlane/screenshots 54 | 55 | #Code Injection 56 | # 57 | # After new code Injection tools there's a generated folder /iOSInjectionProject 58 | # https://github.com/johnno1962/injectionforxcode 59 | 60 | iOSInjectionProject/ -------------------------------------------------------------------------------- /Images/r0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/izotx/JMCFlexibleLayout/502b91f9ece56b6a1ab4bcc77d948a5ddbaf6319/Images/r0.jpg -------------------------------------------------------------------------------- /Images/r1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/izotx/JMCFlexibleLayout/502b91f9ece56b6a1ab4bcc77d948a5ddbaf6319/Images/r1.jpg -------------------------------------------------------------------------------- /Images/r2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/izotx/JMCFlexibleLayout/502b91f9ece56b6a1ab4bcc77d948a5ddbaf6319/Images/r2.jpg -------------------------------------------------------------------------------- /Images/r3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/izotx/JMCFlexibleLayout/502b91f9ece56b6a1ab4bcc77d948a5ddbaf6319/Images/r3.jpg -------------------------------------------------------------------------------- /Images/r4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/izotx/JMCFlexibleLayout/502b91f9ece56b6a1ab4bcc77d948a5ddbaf6319/Images/r4.jpg -------------------------------------------------------------------------------- /Images/r5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/izotx/JMCFlexibleLayout/502b91f9ece56b6a1ab4bcc77d948a5ddbaf6319/Images/r5.jpg -------------------------------------------------------------------------------- /Images/r6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/izotx/JMCFlexibleLayout/502b91f9ece56b6a1ab4bcc77d948a5ddbaf6319/Images/r6.jpg -------------------------------------------------------------------------------- /Images/r7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/izotx/JMCFlexibleLayout/502b91f9ece56b6a1ab4bcc77d948a5ddbaf6319/Images/r7.jpg -------------------------------------------------------------------------------- /Images/r8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/izotx/JMCFlexibleLayout/502b91f9ece56b6a1ab4bcc77d948a5ddbaf6319/Images/r8.jpg -------------------------------------------------------------------------------- /JMCFlexibleDatasource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMCFlexibleDatasource.swift 3 | // JMCFlexibleLayout 4 | // 5 | // Created by Janusz Chudzynski on 7/9/16. 6 | // Copyright © 2016 izotx. All rights reserved. 7 | // 8 | import UIKit 9 | 10 | /**Protocol that needs to be implemented by the cells implemented by the cells to be displayed in the layout*/ 11 | protocol JMCFlexibleCellProtocol{ 12 | func configureWithItem(item:DataSourceItem, indexPath:NSIndexPath) 13 | } 14 | 15 | /**Item Selected Protocol*/ 16 | protocol JMCFlexibleCellSelection { 17 | func cellDidSelected(indexPath:NSIndexPath, item:DataSourceItem) 18 | } 19 | 20 | /**Generic Data source item*/ 21 | class DataSourceItem: NSObject{ 22 | 23 | func getSize()->CGSize{ 24 | return CGSizeZero 25 | } 26 | } 27 | 28 | /**Not that Generic Data source item*/ 29 | class JMCDataSourceItem:DataSourceItem{ 30 | //Image to display in the collection view cell 31 | var image:UIImage? 32 | 33 | // Make sure to override this method to pass the size of the element to display in the collection view cell 34 | override func getSize()->CGSize{ 35 | if let image = image { 36 | return image.size 37 | } 38 | return CGSizeZero 39 | } 40 | } 41 | 42 | 43 | /***Sample FLexible Collection View Cell*/ 44 | class FlexibleCollectionCell : UICollectionViewCell{ 45 | var imageView = UIImageView() 46 | required init?(coder aDecoder: NSCoder) { 47 | super.init(coder: aDecoder) 48 | contentView.addSubview(imageView) 49 | imageView.translatesAutoresizingMaskIntoConstraints = false 50 | self.translatesAutoresizingMaskIntoConstraints = false 51 | 52 | imageView.trailingAnchor.constraintEqualToAnchor(contentView.trailingAnchor).active = true 53 | imageView.leadingAnchor.constraintEqualToAnchor(contentView.leadingAnchor).active = true 54 | imageView.topAnchor.constraintEqualToAnchor(contentView.topAnchor).active = true 55 | imageView.bottomAnchor.constraintEqualToAnchor(contentView.bottomAnchor).active = true 56 | } 57 | 58 | override init(frame: CGRect) { 59 | super.init(frame: frame) 60 | contentView.addSubview(imageView) 61 | imageView.contentMode = .ScaleAspectFit 62 | 63 | imageView.translatesAutoresizingMaskIntoConstraints = false 64 | self.translatesAutoresizingMaskIntoConstraints = false 65 | 66 | imageView.trailingAnchor.constraintEqualToAnchor(contentView.trailingAnchor).active = true 67 | imageView.leadingAnchor.constraintEqualToAnchor(contentView.leadingAnchor).active = true 68 | imageView.topAnchor.constraintEqualToAnchor(contentView.topAnchor).active = true 69 | imageView.bottomAnchor.constraintEqualToAnchor(contentView.bottomAnchor).active = true 70 | 71 | // imageView.layer.borderColor = UIColor.greenColor().CGColor 72 | // imageView.layer.borderWidth = 1.0 73 | // contentView.layer.borderColor = UIColor.redColor().CGColor 74 | // contentView.layer.borderWidth = 1.0 75 | } 76 | 77 | 78 | func configureWithItem(item:DataSourceItem, indexPath:NSIndexPath){ 79 | 80 | if let item = item as? JMCDataSourceItem{ 81 | self.imageView.image = item.image 82 | } 83 | } 84 | 85 | } 86 | 87 | 88 | /*Datasource for the collection view*/ 89 | class JMCFlexibleCollectionViewDataSource: NSObject, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout{ 90 | /**Layout Methods*/ 91 | private let layoutHelper = JMCFlexibleLayout() 92 | /**Collection view*/ 93 | private weak var collectionView:UICollectionView? 94 | 95 | private var cellSizes = [CGSize]() 96 | 97 | private var cellIdentifier:String 98 | 99 | var delegate: JMCFlexibleCellSelection? 100 | 101 | /**Margins around the collection view*/ 102 | var margin:CGFloat = 25{ 103 | didSet{ 104 | 105 | setup() 106 | } 107 | } 108 | 109 | /**Spacing between the cells*/ 110 | var spacing:CGFloat = 14{ 111 | didSet{ 112 | if let c = collectionView 113 | { 114 | print(c.frame) 115 | if spacing * 2 >= c.frame.width{ 116 | spacing = 14 117 | } 118 | } 119 | 120 | setup() 121 | } 122 | } 123 | /**Determines how tall can the row be*/ 124 | var maximumRowHeight:CGFloat = 300{ 125 | didSet{ 126 | setup() 127 | } 128 | } 129 | 130 | /**Data source elements to display*/ 131 | var dataItems = [JMCDataSourceItem](){ 132 | didSet{ 133 | //dataItems = dataItems.filter({$0.image != nil}) 134 | setup() 135 | } 136 | } 137 | 138 | init(collectionView:UICollectionView, cellIdentifier:String) { 139 | self.collectionView = collectionView 140 | self.cellIdentifier = cellIdentifier 141 | super.init() 142 | collectionView.delegate = self 143 | collectionView.dataSource = self 144 | } 145 | 146 | func setup(){ 147 | collectionView?.contentInset = UIEdgeInsets(top: margin, left: margin, bottom: margin, right: margin) 148 | prepareSizes() 149 | collectionView?.reloadData() 150 | } 151 | 152 | 153 | func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat { 154 | return spacing 155 | } 156 | 157 | //MARK: Collection view delegate 158 | func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int 159 | { 160 | return dataItems.count 161 | } 162 | // The cell that is returned must be retrieved from a call to -dequeueReusableCellWithReuseIdentifier:forIndexPath: 163 | 164 | func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell 165 | { 166 | let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as! FlexibleCollectionCell 167 | cell.configureWithItem(dataItems[indexPath.row], indexPath: indexPath) 168 | return cell 169 | } 170 | 171 | func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int 172 | { 173 | return 1 174 | } 175 | 176 | 177 | //MARK: Delegate 178 | func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { 179 | delegate?.cellDidSelected(indexPath, item: dataItems[indexPath.row]) 180 | } 181 | 182 | 183 | 184 | 185 | /**This method generates a dynamic grid based on the image sizes*/ 186 | func collectionView(collectionView: UICollectionView, 187 | layout collectionViewLayout: UICollectionViewLayout, 188 | sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize{ 189 | if cellSizes.count <= indexPath.row { 190 | return CGSizeZero 191 | } 192 | 193 | return cellSizes[indexPath.row] 194 | } 195 | 196 | /**If for any reason you decide to deal with static sizes */ 197 | private func staticSize(indexPath:NSIndexPath) -> CGSize { 198 | guard let collectionView = collectionView else { 199 | return CGSizeZero 200 | } 201 | let width = collectionView.frame.width 202 | let cellWidth = (width - 2 * margin - 1 * spacing ) * 1.0/2.0 203 | let cellHeight = cellWidth 204 | return CGSizeMake(cellWidth, cellHeight) 205 | } 206 | 207 | /**Calculates size of */ 208 | private func prepareSizes(){ 209 | let sizes = dataItems.map({return $0.getSize()}) 210 | let width = CGRectGetWidth(self.collectionView!.frame) - 2 * margin 211 | //Maximum height of the row 212 | let height:CGFloat = maximumRowHeight 213 | layoutHelper.spacing = spacing 214 | /**we should be calling this method only once */ 215 | cellSizes = layoutHelper.generateGrid(sizes, maxWidth: width, maxHeight: height, viewHeight: CGRectGetHeight(collectionView!.frame)).0 216 | } 217 | 218 | 219 | deinit{ 220 | 221 | } 222 | } 223 | 224 | -------------------------------------------------------------------------------- /JMCFlexibleLayout.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | B6951F321D31887900307696 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6951F311D31887900307696 /* AppDelegate.swift */; }; 11 | B6951F341D31887900307696 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6951F331D31887900307696 /* ViewController.swift */; }; 12 | B6951F371D31887900307696 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6951F351D31887900307696 /* Main.storyboard */; }; 13 | B6951F391D31887900307696 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B6951F381D31887900307696 /* Assets.xcassets */; }; 14 | B6951F3C1D31887900307696 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6951F3A1D31887900307696 /* LaunchScreen.storyboard */; }; 15 | B6951F441D318A7800307696 /* JMCFlexibleLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6951F431D318A7800307696 /* JMCFlexibleLayout.swift */; }; 16 | B6951F591D31D08800307696 /* JMCFlexibleDatasource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6951F581D31D08800307696 /* JMCFlexibleDatasource.swift */; }; 17 | B6951F631D31DB2200307696 /* r0.jpg in Resources */ = {isa = PBXBuildFile; fileRef = B6951F5A1D31DB2200307696 /* r0.jpg */; }; 18 | B6951F641D31DB2200307696 /* r1.jpg in Resources */ = {isa = PBXBuildFile; fileRef = B6951F5B1D31DB2200307696 /* r1.jpg */; }; 19 | B6951F651D31DB2200307696 /* r2.jpg in Resources */ = {isa = PBXBuildFile; fileRef = B6951F5C1D31DB2200307696 /* r2.jpg */; }; 20 | B6951F661D31DB2200307696 /* r3.jpg in Resources */ = {isa = PBXBuildFile; fileRef = B6951F5D1D31DB2200307696 /* r3.jpg */; }; 21 | B6951F671D31DB2200307696 /* r4.jpg in Resources */ = {isa = PBXBuildFile; fileRef = B6951F5E1D31DB2200307696 /* r4.jpg */; }; 22 | B6951F681D31DB2200307696 /* r5.jpg in Resources */ = {isa = PBXBuildFile; fileRef = B6951F5F1D31DB2200307696 /* r5.jpg */; }; 23 | B6951F691D31DB2200307696 /* r6.jpg in Resources */ = {isa = PBXBuildFile; fileRef = B6951F601D31DB2200307696 /* r6.jpg */; }; 24 | B6951F6A1D31DB2200307696 /* r7.jpg in Resources */ = {isa = PBXBuildFile; fileRef = B6951F611D31DB2200307696 /* r7.jpg */; }; 25 | B6951F6B1D31DB2200307696 /* r8.jpg in Resources */ = {isa = PBXBuildFile; fileRef = B6951F621D31DB2200307696 /* r8.jpg */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXFileReference section */ 29 | B6951F2E1D31887900307696 /* JMCFlexibleLayout.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = JMCFlexibleLayout.app; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | B6951F311D31887900307696 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 31 | B6951F331D31887900307696 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 32 | B6951F361D31887900307696 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 33 | B6951F381D31887900307696 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 34 | B6951F3B1D31887900307696 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 35 | B6951F3D1D31887900307696 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 36 | B6951F431D318A7800307696 /* JMCFlexibleLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = JMCFlexibleLayout.swift; path = JMCFlexibleLayout/JMCFlexibleLayout.swift; sourceTree = ""; }; 37 | B6951F581D31D08800307696 /* JMCFlexibleDatasource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JMCFlexibleDatasource.swift; sourceTree = ""; }; 38 | B6951F5A1D31DB2200307696 /* r0.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = r0.jpg; path = Images/r0.jpg; sourceTree = ""; }; 39 | B6951F5B1D31DB2200307696 /* r1.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = r1.jpg; path = Images/r1.jpg; sourceTree = ""; }; 40 | B6951F5C1D31DB2200307696 /* r2.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = r2.jpg; path = Images/r2.jpg; sourceTree = ""; }; 41 | B6951F5D1D31DB2200307696 /* r3.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = r3.jpg; path = Images/r3.jpg; sourceTree = ""; }; 42 | B6951F5E1D31DB2200307696 /* r4.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = r4.jpg; path = Images/r4.jpg; sourceTree = ""; }; 43 | B6951F5F1D31DB2200307696 /* r5.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = r5.jpg; path = Images/r5.jpg; sourceTree = ""; }; 44 | B6951F601D31DB2200307696 /* r6.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = r6.jpg; path = Images/r6.jpg; sourceTree = ""; }; 45 | B6951F611D31DB2200307696 /* r7.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = r7.jpg; path = Images/r7.jpg; sourceTree = ""; }; 46 | B6951F621D31DB2200307696 /* r8.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = r8.jpg; path = Images/r8.jpg; sourceTree = ""; }; 47 | /* End PBXFileReference section */ 48 | 49 | /* Begin PBXFrameworksBuildPhase section */ 50 | B6951F2B1D31887900307696 /* Frameworks */ = { 51 | isa = PBXFrameworksBuildPhase; 52 | buildActionMask = 2147483647; 53 | files = ( 54 | ); 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | /* End PBXFrameworksBuildPhase section */ 58 | 59 | /* Begin PBXGroup section */ 60 | B6951F251D31887900307696 = { 61 | isa = PBXGroup; 62 | children = ( 63 | B6951F581D31D08800307696 /* JMCFlexibleDatasource.swift */, 64 | B6951F431D318A7800307696 /* JMCFlexibleLayout.swift */, 65 | B6951F451D318D8400307696 /* Images */, 66 | B6951F301D31887900307696 /* JMCFlexibleLayout */, 67 | B6951F2F1D31887900307696 /* Products */, 68 | ); 69 | sourceTree = ""; 70 | }; 71 | B6951F2F1D31887900307696 /* Products */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | B6951F2E1D31887900307696 /* JMCFlexibleLayout.app */, 75 | ); 76 | name = Products; 77 | sourceTree = ""; 78 | }; 79 | B6951F301D31887900307696 /* JMCFlexibleLayout */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | B6951F311D31887900307696 /* AppDelegate.swift */, 83 | B6951F331D31887900307696 /* ViewController.swift */, 84 | B6951F351D31887900307696 /* Main.storyboard */, 85 | B6951F381D31887900307696 /* Assets.xcassets */, 86 | B6951F3A1D31887900307696 /* LaunchScreen.storyboard */, 87 | B6951F3D1D31887900307696 /* Info.plist */, 88 | ); 89 | path = JMCFlexibleLayout; 90 | sourceTree = ""; 91 | }; 92 | B6951F451D318D8400307696 /* Images */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | B6951F5A1D31DB2200307696 /* r0.jpg */, 96 | B6951F5B1D31DB2200307696 /* r1.jpg */, 97 | B6951F5C1D31DB2200307696 /* r2.jpg */, 98 | B6951F5D1D31DB2200307696 /* r3.jpg */, 99 | B6951F5E1D31DB2200307696 /* r4.jpg */, 100 | B6951F5F1D31DB2200307696 /* r5.jpg */, 101 | B6951F601D31DB2200307696 /* r6.jpg */, 102 | B6951F611D31DB2200307696 /* r7.jpg */, 103 | B6951F621D31DB2200307696 /* r8.jpg */, 104 | ); 105 | name = Images; 106 | sourceTree = ""; 107 | }; 108 | /* End PBXGroup section */ 109 | 110 | /* Begin PBXNativeTarget section */ 111 | B6951F2D1D31887900307696 /* JMCFlexibleLayout */ = { 112 | isa = PBXNativeTarget; 113 | buildConfigurationList = B6951F401D31887900307696 /* Build configuration list for PBXNativeTarget "JMCFlexibleLayout" */; 114 | buildPhases = ( 115 | B6951F2A1D31887900307696 /* Sources */, 116 | B6951F2B1D31887900307696 /* Frameworks */, 117 | B6951F2C1D31887900307696 /* Resources */, 118 | ); 119 | buildRules = ( 120 | ); 121 | dependencies = ( 122 | ); 123 | name = JMCFlexibleLayout; 124 | productName = JMCFlexibleLayout; 125 | productReference = B6951F2E1D31887900307696 /* JMCFlexibleLayout.app */; 126 | productType = "com.apple.product-type.application"; 127 | }; 128 | /* End PBXNativeTarget section */ 129 | 130 | /* Begin PBXProject section */ 131 | B6951F261D31887900307696 /* Project object */ = { 132 | isa = PBXProject; 133 | attributes = { 134 | LastSwiftUpdateCheck = 0730; 135 | LastUpgradeCheck = 0730; 136 | ORGANIZATIONNAME = izotx; 137 | TargetAttributes = { 138 | B6951F2D1D31887900307696 = { 139 | CreatedOnToolsVersion = 7.3; 140 | }; 141 | }; 142 | }; 143 | buildConfigurationList = B6951F291D31887900307696 /* Build configuration list for PBXProject "JMCFlexibleLayout" */; 144 | compatibilityVersion = "Xcode 3.2"; 145 | developmentRegion = English; 146 | hasScannedForEncodings = 0; 147 | knownRegions = ( 148 | en, 149 | Base, 150 | ); 151 | mainGroup = B6951F251D31887900307696; 152 | productRefGroup = B6951F2F1D31887900307696 /* Products */; 153 | projectDirPath = ""; 154 | projectRoot = ""; 155 | targets = ( 156 | B6951F2D1D31887900307696 /* JMCFlexibleLayout */, 157 | ); 158 | }; 159 | /* End PBXProject section */ 160 | 161 | /* Begin PBXResourcesBuildPhase section */ 162 | B6951F2C1D31887900307696 /* Resources */ = { 163 | isa = PBXResourcesBuildPhase; 164 | buildActionMask = 2147483647; 165 | files = ( 166 | B6951F641D31DB2200307696 /* r1.jpg in Resources */, 167 | B6951F651D31DB2200307696 /* r2.jpg in Resources */, 168 | B6951F3C1D31887900307696 /* LaunchScreen.storyboard in Resources */, 169 | B6951F671D31DB2200307696 /* r4.jpg in Resources */, 170 | B6951F391D31887900307696 /* Assets.xcassets in Resources */, 171 | B6951F6A1D31DB2200307696 /* r7.jpg in Resources */, 172 | B6951F691D31DB2200307696 /* r6.jpg in Resources */, 173 | B6951F6B1D31DB2200307696 /* r8.jpg in Resources */, 174 | B6951F661D31DB2200307696 /* r3.jpg in Resources */, 175 | B6951F371D31887900307696 /* Main.storyboard in Resources */, 176 | B6951F681D31DB2200307696 /* r5.jpg in Resources */, 177 | B6951F631D31DB2200307696 /* r0.jpg in Resources */, 178 | ); 179 | runOnlyForDeploymentPostprocessing = 0; 180 | }; 181 | /* End PBXResourcesBuildPhase section */ 182 | 183 | /* Begin PBXSourcesBuildPhase section */ 184 | B6951F2A1D31887900307696 /* Sources */ = { 185 | isa = PBXSourcesBuildPhase; 186 | buildActionMask = 2147483647; 187 | files = ( 188 | B6951F341D31887900307696 /* ViewController.swift in Sources */, 189 | B6951F321D31887900307696 /* AppDelegate.swift in Sources */, 190 | B6951F591D31D08800307696 /* JMCFlexibleDatasource.swift in Sources */, 191 | B6951F441D318A7800307696 /* JMCFlexibleLayout.swift in Sources */, 192 | ); 193 | runOnlyForDeploymentPostprocessing = 0; 194 | }; 195 | /* End PBXSourcesBuildPhase section */ 196 | 197 | /* Begin PBXVariantGroup section */ 198 | B6951F351D31887900307696 /* Main.storyboard */ = { 199 | isa = PBXVariantGroup; 200 | children = ( 201 | B6951F361D31887900307696 /* Base */, 202 | ); 203 | name = Main.storyboard; 204 | sourceTree = ""; 205 | }; 206 | B6951F3A1D31887900307696 /* LaunchScreen.storyboard */ = { 207 | isa = PBXVariantGroup; 208 | children = ( 209 | B6951F3B1D31887900307696 /* Base */, 210 | ); 211 | name = LaunchScreen.storyboard; 212 | sourceTree = ""; 213 | }; 214 | /* End PBXVariantGroup section */ 215 | 216 | /* Begin XCBuildConfiguration section */ 217 | B6951F3E1D31887900307696 /* Debug */ = { 218 | isa = XCBuildConfiguration; 219 | buildSettings = { 220 | ALWAYS_SEARCH_USER_PATHS = NO; 221 | CLANG_ANALYZER_NONNULL = YES; 222 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 223 | CLANG_CXX_LIBRARY = "libc++"; 224 | CLANG_ENABLE_MODULES = YES; 225 | CLANG_ENABLE_OBJC_ARC = YES; 226 | CLANG_WARN_BOOL_CONVERSION = YES; 227 | CLANG_WARN_CONSTANT_CONVERSION = YES; 228 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 229 | CLANG_WARN_EMPTY_BODY = YES; 230 | CLANG_WARN_ENUM_CONVERSION = YES; 231 | CLANG_WARN_INT_CONVERSION = YES; 232 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 233 | CLANG_WARN_UNREACHABLE_CODE = YES; 234 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 235 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 236 | COPY_PHASE_STRIP = NO; 237 | DEBUG_INFORMATION_FORMAT = dwarf; 238 | ENABLE_STRICT_OBJC_MSGSEND = YES; 239 | ENABLE_TESTABILITY = YES; 240 | GCC_C_LANGUAGE_STANDARD = gnu99; 241 | GCC_DYNAMIC_NO_PIC = NO; 242 | GCC_NO_COMMON_BLOCKS = YES; 243 | GCC_OPTIMIZATION_LEVEL = 0; 244 | GCC_PREPROCESSOR_DEFINITIONS = ( 245 | "DEBUG=1", 246 | "$(inherited)", 247 | ); 248 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 249 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 250 | GCC_WARN_UNDECLARED_SELECTOR = YES; 251 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 252 | GCC_WARN_UNUSED_FUNCTION = YES; 253 | GCC_WARN_UNUSED_VARIABLE = YES; 254 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 255 | MTL_ENABLE_DEBUG_INFO = YES; 256 | ONLY_ACTIVE_ARCH = YES; 257 | SDKROOT = iphoneos; 258 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 259 | TARGETED_DEVICE_FAMILY = "1,2"; 260 | }; 261 | name = Debug; 262 | }; 263 | B6951F3F1D31887900307696 /* Release */ = { 264 | isa = XCBuildConfiguration; 265 | buildSettings = { 266 | ALWAYS_SEARCH_USER_PATHS = NO; 267 | CLANG_ANALYZER_NONNULL = YES; 268 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 269 | CLANG_CXX_LIBRARY = "libc++"; 270 | CLANG_ENABLE_MODULES = YES; 271 | CLANG_ENABLE_OBJC_ARC = YES; 272 | CLANG_WARN_BOOL_CONVERSION = YES; 273 | CLANG_WARN_CONSTANT_CONVERSION = YES; 274 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 275 | CLANG_WARN_EMPTY_BODY = YES; 276 | CLANG_WARN_ENUM_CONVERSION = YES; 277 | CLANG_WARN_INT_CONVERSION = YES; 278 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 279 | CLANG_WARN_UNREACHABLE_CODE = YES; 280 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 281 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 282 | COPY_PHASE_STRIP = NO; 283 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 284 | ENABLE_NS_ASSERTIONS = NO; 285 | ENABLE_STRICT_OBJC_MSGSEND = YES; 286 | GCC_C_LANGUAGE_STANDARD = gnu99; 287 | GCC_NO_COMMON_BLOCKS = YES; 288 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 289 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 290 | GCC_WARN_UNDECLARED_SELECTOR = YES; 291 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 292 | GCC_WARN_UNUSED_FUNCTION = YES; 293 | GCC_WARN_UNUSED_VARIABLE = YES; 294 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 295 | MTL_ENABLE_DEBUG_INFO = NO; 296 | SDKROOT = iphoneos; 297 | TARGETED_DEVICE_FAMILY = "1,2"; 298 | VALIDATE_PRODUCT = YES; 299 | }; 300 | name = Release; 301 | }; 302 | B6951F411D31887900307696 /* Debug */ = { 303 | isa = XCBuildConfiguration; 304 | buildSettings = { 305 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 306 | INFOPLIST_FILE = JMCFlexibleLayout/Info.plist; 307 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 308 | PRODUCT_BUNDLE_IDENTIFIER = com.izotx.JMCFlexibleLayout; 309 | PRODUCT_NAME = "$(TARGET_NAME)"; 310 | }; 311 | name = Debug; 312 | }; 313 | B6951F421D31887900307696 /* Release */ = { 314 | isa = XCBuildConfiguration; 315 | buildSettings = { 316 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 317 | INFOPLIST_FILE = JMCFlexibleLayout/Info.plist; 318 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 319 | PRODUCT_BUNDLE_IDENTIFIER = com.izotx.JMCFlexibleLayout; 320 | PRODUCT_NAME = "$(TARGET_NAME)"; 321 | }; 322 | name = Release; 323 | }; 324 | /* End XCBuildConfiguration section */ 325 | 326 | /* Begin XCConfigurationList section */ 327 | B6951F291D31887900307696 /* Build configuration list for PBXProject "JMCFlexibleLayout" */ = { 328 | isa = XCConfigurationList; 329 | buildConfigurations = ( 330 | B6951F3E1D31887900307696 /* Debug */, 331 | B6951F3F1D31887900307696 /* Release */, 332 | ); 333 | defaultConfigurationIsVisible = 0; 334 | defaultConfigurationName = Release; 335 | }; 336 | B6951F401D31887900307696 /* Build configuration list for PBXNativeTarget "JMCFlexibleLayout" */ = { 337 | isa = XCConfigurationList; 338 | buildConfigurations = ( 339 | B6951F411D31887900307696 /* Debug */, 340 | B6951F421D31887900307696 /* Release */, 341 | ); 342 | defaultConfigurationIsVisible = 0; 343 | }; 344 | /* End XCConfigurationList section */ 345 | }; 346 | rootObject = B6951F261D31887900307696 /* Project object */; 347 | } 348 | -------------------------------------------------------------------------------- /JMCFlexibleLayout.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /JMCFlexibleLayout.xcodeproj/xcuserdata/januszchudzynski.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | JMCFlexibleLayout.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | B6951F2D1D31887900307696 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /JMCFlexibleLayout/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // JMCFlexibleLayout 4 | // 5 | // Created by Janusz Chudzynski on 7/9/16. 6 | // Copyright © 2016 izotx. 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: [NSObject: AnyObject]?) -> 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 throttle down OpenGL ES frame rates. 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 inactive 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 | -------------------------------------------------------------------------------- /JMCFlexibleLayout/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 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /JMCFlexibleLayout/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 | -------------------------------------------------------------------------------- /JMCFlexibleLayout/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 | -------------------------------------------------------------------------------- /JMCFlexibleLayout/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /JMCFlexibleLayout/JMCFlexibleLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMCFlexibleLayout.swift 3 | // PerfecTour 4 | // 5 | // Created by Janusz Chudzynski on 2/18/16. 6 | // Copyright © 2016 PerfecTour. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | /**Class responsible for calculating size of the cells based on available width and maximum height*/ 11 | class JMCFlexibleLayout{ 12 | 13 | var spacing:CGFloat = 10{ 14 | didSet{ 15 | sizes = generateGrid(sizes, maxWidth: maxWidth, maxHeight: maxHeight).0 16 | } 17 | } 18 | 19 | /**Defualt maximum height of line */ 20 | var maxHeight:CGFloat = 300{ 21 | didSet{ 22 | sizes = generateGrid(sizes, maxWidth: maxWidth, maxHeight: maxHeight).0 23 | } 24 | } 25 | 26 | /**Defualt maximum width of the container */ 27 | var maxWidth:CGFloat=0{ 28 | didSet{ 29 | sizes = generateGrid(sizes, maxWidth: maxWidth, maxHeight: maxHeight).0 30 | } 31 | } 32 | 33 | 34 | 35 | /**Views*/ 36 | var views = [UIView]() 37 | var sizes = [CGSize](){ 38 | didSet{ 39 | sizes = generateGrid(sizes, maxWidth: maxWidth, maxHeight: maxHeight).0 40 | } 41 | } 42 | 43 | /**Get's size of the item based on inded in array*/ 44 | func getSizeOfItem(index:Int)->CGSize{ 45 | return sizes[index] 46 | } 47 | 48 | /** 49 | Generates new frames and assigns them to passed views 50 | - paramater views - array of views 51 | - paramater height height of the row constraining the view 52 | - paramater rowY - y coordinate of the view 53 | 54 | */ 55 | func generateNewFrames(views:[UIView], height:CGFloat, rowY:CGFloat){ 56 | //add vertical spacing 57 | var x:CGFloat = spacing 58 | for v in views{ 59 | //just to have a different colors 60 | v.backgroundColor = JMCFlexibleLayout.generateRandomColor() 61 | //calculate frames 62 | let w = calculateNewWidth(v, height: height) 63 | let frame = CGRectMake(x, rowY, w, height) 64 | x = spacing + x + w 65 | v.frame = frame 66 | } 67 | } 68 | 69 | /**Generates new sizes for array of views constrained to a given height - Represents the single row*/ 70 | func generateNewSizes(sizes:[CGSize], height:CGFloat)->[CGSize]{ 71 | var adjustedSizes = [CGSize]() 72 | for v in sizes{ 73 | //calculate frames 74 | let width = calculateNewWidth(v, height: height) 75 | adjustedSizes.append(CGSizeMake(width,height)) 76 | } 77 | return adjustedSizes 78 | } 79 | 80 | 81 | /**Renders Grid of UIViews on a screen*/ 82 | func renderGrid(grid:[Int:[UIView]],inView view:UIView){ 83 | for (_,views) in grid{ 84 | for v in views{ 85 | view.addSubview(v) 86 | } 87 | } 88 | } 89 | 90 | 91 | /**Calculates height of the row based on passed array of CGSizes*/ 92 | func calculateNewHeight(views:[CGSize], maxWidth:CGFloat)->CGFloat{ 93 | var aspect:CGFloat = 0 94 | for v in views{ 95 | let h = v.height 96 | let w = v.width 97 | let newAspect = w/h 98 | aspect += newAspect 99 | } 100 | return maxWidth/aspect 101 | } 102 | 103 | 104 | /*Debugging */ 105 | func printRow( row:[CGSize])->[CGSize]{ 106 | var sum:CGFloat = 0 107 | var newSum:CGFloat = 0 108 | 109 | let newRow = row.map({CGSizeMake($0.width * 0.999, $0.height)}) 110 | var index = 0 111 | for e in row{ 112 | sum += e.width 113 | 114 | newSum += e.width 115 | index += 1 116 | } 117 | 118 | 119 | return newRow 120 | } 121 | 122 | 123 | 124 | /**Function that generates a grid based on passed dimensions (Sizes)*/ 125 | func generateGrid(sizes:[CGSize], maxWidth:CGFloat, maxHeight:CGFloat, viewHeight:CGFloat = 0 )->([CGSize],[[CGSize]]){ 126 | //current row 127 | // another item 128 | // while height > threshold keep adding 129 | var grid = [CGSize]() 130 | var twoDGrid = [[CGSize]]() 131 | 132 | var currentRow = [CGSize]() 133 | var threshold:CGFloat = maxHeight 134 | 135 | //For housekeeping purposes what if number of cells is less than 4? 136 | if sizes.count <= 4{ 137 | //threshold = 138 | threshold = viewHeight / 2.0 139 | } 140 | 141 | for v in sizes{ 142 | var newArray:[CGSize] = currentRow 143 | newArray.append(v) 144 | 145 | //check the adjusted width of all elements based on spacing an number of elements 146 | let adjustedMaxWidth = maxWidth - CGFloat(newArray.count-1) * spacing 147 | let height = calculateNewHeight(newArray, maxWidth: adjustedMaxWidth ) 148 | 149 | if height <= threshold{ 150 | //Magic number for some reason it doesn't fit if we don't shrink it here 151 | let adjustedSizes = generateNewSizes(newArray, height: 0.999 * height) 152 | // let adjustedSizes = generateNewSizes(newArray, height: 1 * height) 153 | 154 | grid = grid + adjustedSizes 155 | 156 | twoDGrid.append(adjustedSizes) 157 | 158 | currentRow.removeAll() 159 | } 160 | else{ 161 | currentRow = newArray 162 | 163 | } 164 | } 165 | 166 | /**House keeping*/ 167 | let adjustedSizes = generateNewSizes(currentRow, height: threshold) 168 | grid = grid + adjustedSizes 169 | 170 | 171 | return (grid, twoDGrid) 172 | } 173 | 174 | 175 | 176 | /** 177 | Generates a two dimensional grid of UIViews 178 | // step 1 get list of all views 179 | // add it to the processing one by one if the height is more than threshold keep adding 180 | // if it is less then a threshold render a row 181 | */ 182 | 183 | func generateGrid(views:[UIView], maxWidth:CGFloat, maxHeight:CGFloat)->[Int:[UIView]]{ 184 | //current row 185 | // another item 186 | // while height > threshold keep adding 187 | var grid = [Int:[UIView]]() 188 | var currentRow = [UIView]() 189 | 190 | //Maximum height threshold 191 | var threshold:CGFloat = maxHeight 192 | 193 | //Maximum height threshold whenever items are less th 194 | if views.count < 4{ 195 | threshold = 400 196 | } 197 | 198 | var rowCount = 0 199 | var rowY:CGFloat = 0// keep track of the row Y coordinates 200 | 201 | 202 | for v in views{ 203 | var newArray:[UIView] = currentRow 204 | newArray.append(v) 205 | let adjustedMaxWidth = maxWidth - CGFloat(newArray.count + 1) * spacing 206 | 207 | 208 | let height = calculateNewHeight(newArray, maxWidth: adjustedMaxWidth ) 209 | 210 | if height <= threshold{ 211 | grid[rowCount] = newArray 212 | newArray.count 213 | //increment row number 214 | rowCount += 1 215 | //update frames based on height 216 | generateNewFrames(newArray, height: height, rowY: rowY + spacing) 217 | rowY += height + spacing 218 | //reset the row 219 | currentRow.removeAll() 220 | } 221 | else{ 222 | currentRow = newArray 223 | } 224 | } 225 | 226 | /**House keeping*/ 227 | generateNewFrames(currentRow, height:threshold, rowY: rowY + spacing) 228 | grid[rowCount] = currentRow 229 | 230 | currentRow.count 231 | 232 | 233 | return grid 234 | } 235 | 236 | 237 | /**Scales entire UIIMage to fit the size - it doesn't care about the aspect ratio*/ 238 | func scaleUIImageToSize(let image: UIImage, let size: CGSize) -> UIImage { 239 | let hasAlpha = false 240 | let scale: CGFloat = 0.0 // Automatically use scale factor of main screen 241 | 242 | UIGraphicsBeginImageContextWithOptions(size, !hasAlpha, scale) 243 | image.drawInRect(CGRect(origin: CGPointZero, size: size)) 244 | 245 | let scaledImage = UIGraphicsGetImageFromCurrentImageContext() 246 | UIGraphicsEndImageContext() 247 | 248 | return scaledImage 249 | } 250 | 251 | /**Returns size of the image scaled to fit square of side widht equal to maxSide*/ 252 | func scaledSize(let image: UIImage, maxSide:CGFloat) -> CGSize { 253 | 254 | 255 | let w = image.size.width 256 | let h = image.size.height 257 | var scaleFactor:CGFloat = 0 258 | var newWidth:CGFloat = 0 259 | var newHeight:CGFloat = 0 260 | 261 | if max(w,h) < maxSide{ 262 | return image.size 263 | } 264 | 265 | if w > h { 266 | scaleFactor = maxSide / w 267 | 268 | } 269 | else{ 270 | scaleFactor = maxSide / h 271 | } 272 | newWidth = w * scaleFactor 273 | newHeight = h * scaleFactor 274 | 275 | return CGSizeMake(newWidth, newHeight) 276 | 277 | } 278 | 279 | 280 | /**Calculates new height of the row for given array of views */ 281 | func calculateNewHeight(views:[UIView], maxWidth:CGFloat)->CGFloat{ 282 | let sizes:[CGSize] = views.map({(v) in return v.frame.size}) 283 | return calculateNewHeight(sizes, maxWidth: maxWidth) 284 | } 285 | 286 | func calculateNewWidth(view:UIView, height:CGFloat)->CGFloat{ 287 | 288 | let w = CGRectGetWidth(view.frame ) * height / CGRectGetHeight(view.frame) 289 | return w 290 | } 291 | 292 | func calculateNewWidth(size:CGSize, height:CGFloat)->CGFloat{ 293 | 294 | let w = size.width * height / size.height 295 | return w 296 | } 297 | 298 | /** 299 | Generates random views 300 | - parameter count number of views to generate 301 | - returns: generated views 302 | */ 303 | 304 | func generateRandomViews(count:Int)->[UIView]{ 305 | var views = [UIView]() 306 | for _ in 0..CGRect{ 322 | let x:CGFloat = 0 323 | let y:CGFloat = 0 324 | let width:CGFloat = 400 + CGFloat(arc4random_uniform(500)) 325 | let height:CGFloat = 300 + CGFloat(arc4random_uniform(500)) 326 | let frame = CGRectMake(x, y, width, height) 327 | return frame 328 | } 329 | 330 | 331 | 332 | /**Generates random color*/ 333 | static func generateRandomColor()->UIColor{ 334 | let r = Float(Float(arc4random()) / Float(UINT32_MAX)) 335 | let g = Float(Float(arc4random()) / Float(UINT32_MAX)) 336 | let b = Float(Float(arc4random()) / Float(UINT32_MAX)) 337 | return UIColor(colorLiteralRed: r, green: g, blue: b, alpha: 1) 338 | } 339 | 340 | } 341 | -------------------------------------------------------------------------------- /JMCFlexibleLayout/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // JMCFlexibleLayout 4 | // 5 | // Created by Janusz Chudzynski on 7/9/16. 6 | // Copyright © 2016 izotx. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /**var classString = NSStringFromClass(self.dynamicType) 12 | Using that string, you can create an instance of your Swift class by executing: 13 | 14 | var anyobjectype : AnyObject.Type = NSClassFromString(classString) 15 | var nsobjectype : NSObject.Type = anyobjectype as NSObject.Type 16 | var rec: AnyObject = nsobjectype()**/ 17 | 18 | 19 | 20 | class ViewController: UIViewController{ 21 | /**Instance of the JMC Flexible collection view data source */ 22 | var datasource: JMCFlexibleCollectionViewDataSource? 23 | 24 | /**Outlets*/ 25 | @IBOutlet weak var spacingTextField: UITextField! 26 | @IBOutlet weak var marginTextField: UITextField! 27 | @IBOutlet weak var maximumHeightTextField: UITextField! 28 | @IBOutlet weak var collectionView: UICollectionView! 29 | @IBOutlet weak var collectionControls: UIStackView! 30 | 31 | var tapGesture:UITapGestureRecognizer? 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | 36 | /**Register collection view cell */ 37 | collectionView.registerClass(FlexibleCollectionCell.self, forCellWithReuseIdentifier: "cell") 38 | 39 | /** Create an instance of the flexible datasource make sure to pass here the collection view and cell identifier */ 40 | datasource = JMCFlexibleCollectionViewDataSource(collectionView: collectionView, cellIdentifier:"cell") 41 | 42 | //prepare items to display in the collection view 43 | var dataSourceItems = [JMCDataSourceItem]() 44 | 45 | for i in 0...8{ 46 | let item = JMCDataSourceItem() 47 | item.image = UIImage(named: "r\(i).jpg") 48 | dataSourceItems.append(item) 49 | } 50 | 51 | //assign the items to the datasource 52 | datasource?.dataItems = dataSourceItems 53 | 54 | let notificationCenter = NSNotificationCenter.defaultCenter() 55 | notificationCenter.addObserver( 56 | self, 57 | selector: #selector(updateUI), 58 | name:UITextFieldTextDidChangeNotification, 59 | object: nil 60 | ) 61 | 62 | 63 | collectionControls.hidden = true 64 | tapGesture = UITapGestureRecognizer(target: self, action: #selector(viewTapped)) 65 | tapGesture?.numberOfTapsRequired = 1 66 | view.addGestureRecognizer(tapGesture!) 67 | } 68 | 69 | /**Show/hide the controls*/ 70 | func viewTapped(){ 71 | collectionControls.hidden = !collectionControls.hidden 72 | } 73 | 74 | override func viewDidLayoutSubviews() { 75 | //At this point all the views have their final dimensions so the size calculations will be accurate 76 | datasource?.setup() 77 | } 78 | 79 | 80 | /**Updates UI - example of what happens when you play with paramaters. */ 81 | func updateUI(){ 82 | var spacing = CGFloat((spacingTextField.text! as NSString).floatValue) 83 | let margin = CGFloat((marginTextField.text! as NSString).floatValue) 84 | var maxH = CGFloat(( maximumHeightTextField.text! as NSString).floatValue) 85 | 86 | //min row height 50 so it won'd dissappear from view 87 | if maxH < 50 { 88 | maxH = 50 89 | } 90 | 91 | if spacing < 10 { 92 | spacing = 10 93 | } 94 | 95 | 96 | datasource?.maximumRowHeight = maxH 97 | datasource?.margin = margin 98 | datasource?.spacing = spacing 99 | } 100 | 101 | 102 | func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool { 103 | updateUI() 104 | 105 | return true 106 | } 107 | 108 | 109 | override func didReceiveMemoryWarning() { 110 | super.didReceiveMemoryWarning() 111 | // Dispose of any resources that can be recreated. 112 | } 113 | 114 | } 115 | 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JMCFlexibleCollectionLayout 2 | > JMCFlexibleCollectionLayout is a iOS library that autoresizes collection view cells so it can display images without changing their aspect ratio. 3 | 4 | 5 | 6 | 7 | 8 | The collection view is a great UI element but it doesn't handle elements with various content sizes elegantly. JMCFlexibleCollectionLayout based on customizable paramaters like spacing between cells, margins, and maximum row height calculates and displays a collection view grid with elements that maintain their original aspect ratio. It serves role of both UICollectionViewDataSource and UICollectionViewDelegate. 9 | 10 | ![](iPadScreenshot.png) 11 | 12 | ![](iPadRetina.gif) 13 | 14 | ## Installation 15 | 16 | iOS: 17 | 18 | ```sh 19 | copy JMCFlexibleLayout.swift and JMCFlexibleDataSource.swift to your project 20 | ``` 21 | 22 | 23 | ## Usage example 24 | 25 | ###Step 1 Configure Collection View 26 | At first you need to setup a collection view. You can create it programmatically or using Storyboards. If you look at the example project, collection view is added using Storyboard. 27 | 28 | ```swift 29 | class ViewController: UIViewController{ 30 | @IBOutlet weak var collectionView: UICollectionView! 31 | ``` 32 | 33 | In the sample project I created and registered cell programmatically. You can do it using storyboards or programmtically as well as long as the cell extends the FlexibleCollectionCell class. 34 | 35 | ```swift 36 | 37 | /**Register collection view cell */ 38 | collectionView.registerClass(FlexibleCollectionCell.self, forCellWithReuseIdentifier: "cell") 39 | ``` 40 | 41 | ###Step 2 42 | Create an instance of the library and initialize it. 43 | 44 | To initialize library you have to pass an instance of UICollectionView and the unique identifier of collection view cell. 45 | 46 | ```swift 47 | class ViewController: UIViewController{ 48 | /**Instance of the JMC Flexible collection view data source */ 49 | var datasource: JMCFlexibleCollectionViewDataSource? 50 | 51 | @IBOutlet weak var collectionView: UICollectionView! 52 | 53 | override func viewDidLoad() { 54 | super.viewDidLoad() 55 | 56 | /**Register collection view cell */ 57 | collectionView.registerClass(FlexibleCollectionCell.self, forCellWithReuseIdentifier: "cell") 58 | 59 | /** Create an instance of the flexible datasource make sure to pass here the collection view and cell identifier */ 60 | datasource = JMCFlexibleCollectionViewDataSource(collectionView: collectionView, cellIdentifier:"cell") 61 | ``` 62 | 63 | 64 | ###Step 3 Provide data source items 65 | 66 | Data source items need to inherit from JMCDataSourceItem class. Good place to do it is in the viewDidLoad method. 67 | 68 | ```Swift 69 | 70 | //prepare items to display in the collection view 71 | var dataSourceItems = [JMCDataSourceItem]() 72 | for i in 0...8{ 73 | let item = JMCDataSourceItem() 74 | item.image = UIImage(named: "r\(i).jpg") 75 | dataSourceItems.append(item) 76 | } 77 | 78 | //assign the items to the datasource 79 | datasource?.dataItems = dataSourceItems 80 | ``` 81 | 82 | ###Step 4 Call the setup method 83 | That's important. At the point when the viewDidLayoutSubviews is called, the iOS knows exactly what will be the sizes of UI elements. If called prior this point, library can produce unexpected results. 84 | ```Swift 85 | override func viewDidLayoutSubviews() { 86 | //At this point all the views have their final dimensions so the size calculations will be accurate 87 | datasource?.setup() 88 | } 89 | ``` 90 | 91 | ### Step 5 92 | Optional Settings 93 | You can control the look of the collection view by changing values of the following paramaters: 94 | 95 | * maximumRowHeight - what's the maximum allowed height of the row 96 | * margin margin around the edges of collection's view content 97 | * spacing = spacing between the cells (rows and columns) - important currently values under 10 won't work correctly. 98 | 99 | 100 | ## Customization 101 | Library comes with two data structures that can be subclassed to create your custom UI. 102 | 103 | ### Subclassing DataSourceItem provided in the example project. 104 | The items that will provide the content to collection cells view have to be subclassed from DataSourceItem class. In your implementation you need to overwrite the getSize method, and return the size of the element. For example MyCustomDataSourceItem class is a subclass of DataSourceItem class that has an instance of UIImage associated with it: 105 | 106 | ```swift 107 | class MyCustomDataSourceItem:DataSourceItem{ 108 | //Image to display in the collection view cell 109 | var image:UIImage? 110 | var description:String? 111 | 112 | // Make sure to override this method to pass the size of the element to display in the collection view cell 113 | override func getSize()->CGSize{ 114 | if let image = image { 115 | return image.size 116 | } 117 | return CGSizeZero 118 | } 119 | } 120 | ``` 121 | 122 | ### Creating custom collection view cells . 123 | To do it you have to create a data structure that inherits from FlexibleCollectionCell. 124 | If you decide to use your own cell, you have to override configureWithItem method and customize the look of your cell. 125 | 126 | ```swift 127 | class MyFlexibleCollectionCell : UICollectionViewCell{ 128 | @IBOutlet weak var imageView: UIImageView! 129 | @IBOutlet weak var label: UILabel! 130 | func configureWithItem(item:DataSourceItem, indexPath:NSIndexPath){ 131 | if let item = item as? MyDataSourceItem{ 132 | self.imageView.image = item.image 133 | self.label.text = item.description 134 | } 135 | } 136 | } 137 | ``` 138 | 139 | 140 | ## Release History 141 | 142 | * 0.1 July 2016 143 | * Candidate for 1st release 144 | 145 | ## Meta 146 | 147 | Janusz Chudzynski Izotx LLC – [@appzzman](https://twitter.com/appzzman) – janusz@izotx.com 148 | 149 | Distributed under the BSD license. See ``LICENSE`` for more information. 150 | 151 | [https://github.com/yourname/github-link](https://github.com/izotx/JMCFlexibleLayout) 152 | 153 | Drawing 154 | Drawing 155 | 156 | 157 | 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /iPadGif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/izotx/JMCFlexibleLayout/502b91f9ece56b6a1ab4bcc77d948a5ddbaf6319/iPadGif.gif -------------------------------------------------------------------------------- /iPadRetina.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/izotx/JMCFlexibleLayout/502b91f9ece56b6a1ab4bcc77d948a5ddbaf6319/iPadRetina.gif -------------------------------------------------------------------------------- /iPadScreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/izotx/JMCFlexibleLayout/502b91f9ece56b6a1ab4bcc77d948a5ddbaf6319/iPadScreenshot.png -------------------------------------------------------------------------------- /iPhoneScreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/izotx/JMCFlexibleLayout/502b91f9ece56b6a1ab4bcc77d948a5ddbaf6319/iPhoneScreenshot.png -------------------------------------------------------------------------------- /iPhoneScreenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/izotx/JMCFlexibleLayout/502b91f9ece56b6a1ab4bcc77d948a5ddbaf6319/iPhoneScreenshot2.png --------------------------------------------------------------------------------