├── .gitignore ├── InteractiveGrid ├── InteractiveGrid-SwiftUI │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── ContentView.swift │ ├── InteractiveGridApp.swift │ ├── InteractiveGrid_SwiftUI.entitlements │ ├── Model.swift │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json ├── InteractiveGrid-UIKit │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── ContentView.swift │ ├── DefaultDynamicLayoutGroupProvider.swift │ ├── DynamicLayoutGroupProvider.swift │ ├── GridCell.swift │ ├── GridCollectionViewCell.swift │ ├── InteractiveGridApp.swift │ ├── InteractiveGridViewController.swift │ ├── InteractiveGridViewControllerRepresentable.swift │ ├── Model.swift │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json ├── InteractiveGrid.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ └── InteractiveGrid.xcscheme └── Playground │ └── main.swift ├── LICENSE ├── README.md └── goals.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | .DS_Store 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | 29 | ## App packaging 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # 40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 41 | # Packages/ 42 | # Package.pins 43 | # Package.resolved 44 | # *.xcodeproj 45 | # 46 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 47 | # hence it is not needed unless you have added a package configuration file to your project 48 | # .swiftpm 49 | 50 | .build/ 51 | 52 | # CocoaPods 53 | # 54 | # We recommend against adding the Pods directory to your .gitignore. However 55 | # you should judge for yourself, the pros and cons are mentioned at: 56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 57 | # 58 | # Pods/ 59 | # 60 | # Add this line if you want to avoid checking in source code from the Xcode workspace 61 | # *.xcworkspace 62 | 63 | # Carthage 64 | # 65 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 66 | # Carthage/Checkouts 67 | 68 | Carthage/Build/ 69 | 70 | # Accio dependency management 71 | Dependencies/ 72 | .accio/ 73 | 74 | # fastlane 75 | # 76 | # It is recommended to not store the screenshots in the git repo. 77 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 78 | # For more information about the recommended setup visit: 79 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 80 | 81 | fastlane/report.xml 82 | fastlane/Preview.html 83 | fastlane/screenshots/**/*.png 84 | fastlane/test_output 85 | 86 | # Code Injection 87 | # 88 | # After new code Injection tools there's a generated folder /iOSInjectionProject 89 | # https://github.com/johnno1962/injectionforxcode 90 | 91 | iOSInjectionProject/ 92 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-SwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-SwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "1x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "2x", 16 | "size" : "16x16" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "1x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "2x", 26 | "size" : "32x32" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "2x", 36 | "size" : "128x128" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "scale" : "2x", 56 | "size" : "512x512" 57 | } 58 | ], 59 | "info" : { 60 | "author" : "xcode", 61 | "version" : 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-SwiftUI/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-SwiftUI/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Algorithms 3 | import UniformTypeIdentifiers 4 | 5 | 6 | final class Container: ObservableObject { 7 | @Published var data: [Model] 8 | 9 | init() { 10 | data = Model.makeMixNMatch() 11 | } 12 | } 13 | 14 | struct ContentView: View { 15 | 16 | @StateObject 17 | private var container = Container() 18 | 19 | @State 20 | private var shouldEnableContextMenu = true 21 | 22 | var body: some View { 23 | ZStack { 24 | LinearGradient(colors: [.mint, .orange, .blue], startPoint: .topLeading, endPoint: .bottomTrailing) 25 | .overlay(.ultraThinMaterial) 26 | .edgesIgnoringSafeArea(.all) 27 | 28 | InteractiveGrid(container: container) 29 | } 30 | 31 | .toolbar { 32 | ToolbarItem { 33 | Menu("Demos") { 34 | Button("All Compact") { 35 | withAnimation { 36 | container.data = Model.makeAllCompact() 37 | } 38 | 39 | } 40 | 41 | Button("All Regular") { 42 | withAnimation { 43 | container.data = Model.makeAllRegular() 44 | } 45 | } 46 | 47 | Button("Mix-n-match") { 48 | withAnimation { 49 | container.data = Model.makeMixNMatch() 50 | } 51 | } 52 | 53 | Button("Random") { 54 | withAnimation { 55 | container.data = Model.makeRandom() 56 | } 57 | } 58 | 59 | Button("Shuffle") { 60 | withAnimation { 61 | container.data.append(Model(value: 100, style: .compact, allowsContextMenu: false)) 62 | } 63 | 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | extension Model { 72 | 73 | static func makeAllCompact() -> [Model] { 74 | return [ 75 | Model(value: 1, style: .compact, allowsContextMenu: true), 76 | Model(value: 2, style: .compact, allowsContextMenu: true), 77 | Model(value: 3, style: .compact, allowsContextMenu: true), 78 | Model(value: 4, style: .compact, allowsContextMenu: true), 79 | Model(value: 5, style: .compact, allowsContextMenu: true), 80 | Model(value: 6, style: .compact, allowsContextMenu: true) 81 | ] 82 | } 83 | 84 | static func makeAllRegular() -> [Model] { 85 | return [ 86 | Model(value: 1, style: .regular, allowsContextMenu: false), 87 | Model(value: 2, style: .regular, allowsContextMenu: false), 88 | Model(value: 3, style: .regular, allowsContextMenu: false), 89 | Model(value: 4, style: .regular, allowsContextMenu: false), 90 | Model(value: 5, style: .regular, allowsContextMenu: false), 91 | Model(value: 6, style: .regular, allowsContextMenu: false) 92 | ] 93 | } 94 | 95 | static func makeMixNMatch() -> [Model] { 96 | return [ 97 | Model(value: 1, style: .regular, allowsContextMenu: true), 98 | Model(value: 2, style: .compact, allowsContextMenu: true), 99 | Model(value: 3, style: .compact, allowsContextMenu: true), 100 | Model(value: 4, style: .compact, allowsContextMenu: true), 101 | Model(value: 5, style: .compact, allowsContextMenu: true), 102 | Model(value: 6, style: .regular, allowsContextMenu: true), 103 | Model(value: 7, style: .compact, allowsContextMenu: true), 104 | Model(value: 8, style: .compact, allowsContextMenu: true), 105 | Model(value: 9, style: .regular, allowsContextMenu: true) 106 | ] 107 | } 108 | 109 | static func makeRandom() -> [Model] { 110 | let compactModels = (0..: View where Content: View { 129 | 130 | private let content: () -> Content 131 | 132 | @State 133 | private var shouldScale: Bool = false 134 | 135 | init(@ViewBuilder content: @escaping () -> Content) { 136 | self.content = content 137 | } 138 | 139 | var body: some View { 140 | ZStack { 141 | content() 142 | .padding() 143 | .background(Color.blue) 144 | // .contextMenu(menuItems: { 145 | // Button("Go") { 146 | // 147 | // } 148 | // Button("Stop") { 149 | // 150 | // } 151 | // }) 152 | 153 | } 154 | 155 | .frame(maxWidth: .infinity, maxHeight: .infinity) 156 | .background(Color.blue) 157 | .cornerRadius(16) 158 | .scaleEffect(x: shouldScale ? 0.9 : 1.0, y: shouldScale ? 0.9 : 1.0, anchor: .center) 159 | 160 | 161 | 162 | } 163 | } 164 | 165 | 166 | struct InteractiveGrid: View { 167 | 168 | @ObservedObject 169 | var container: Container 170 | 171 | @State 172 | private var dragging: Model? 173 | 174 | var body: some View { 175 | ScrollView { 176 | Grid { 177 | let chunked = container.data 178 | .chunks(ofCount: 2) 179 | 180 | 181 | ForEach(chunked, id: \.self) { chunk in 182 | switch (chunk.first?.style, chunk.last?.style) { 183 | 184 | case (.compact, .compact): 185 | GridRow { 186 | Gadget { 187 | GridCell(model: chunk.first!) 188 | } 189 | 190 | Gadget { 191 | GridCell(model: chunk.last!) 192 | } 193 | 194 | } 195 | .aspectRatio(1, contentMode: .fill) 196 | case (.compact, .none): 197 | GridRow { 198 | Gadget { 199 | GridCell(model: chunk.first!) 200 | } 201 | } 202 | .aspectRatio(1, contentMode: .fill) 203 | case (.compact, .regular): 204 | GridRow { 205 | Gadget { 206 | GridCell(model: chunk.first!) 207 | } 208 | } 209 | .aspectRatio(1, contentMode: .fill) 210 | 211 | GridRow { 212 | Gadget { 213 | GridCell(model: chunk.last!) 214 | } 215 | } 216 | .gridCellColumns(2) 217 | .aspectRatio(2, contentMode: .fill) 218 | case (.regular, .compact): 219 | GridRow { 220 | Gadget { 221 | GridCell(model: chunk.first!) 222 | } 223 | } 224 | .gridCellColumns(2) 225 | .aspectRatio(2, contentMode: .fill) 226 | 227 | GridRow { 228 | Gadget { 229 | GridCell(model: chunk.last!) 230 | } 231 | } 232 | .gridCellColumns(2) 233 | .aspectRatio(2, contentMode: .fill) 234 | 235 | case (.regular, .regular): 236 | GridRow { 237 | Gadget { 238 | GridCell(model: chunk.first!) 239 | } 240 | } 241 | .gridCellColumns(2) 242 | .aspectRatio(2, contentMode: .fill) 243 | 244 | GridRow { 245 | Gadget { 246 | GridCell(model: chunk.last!) 247 | } 248 | } 249 | .gridCellColumns(2) 250 | .aspectRatio(2, contentMode: .fill) 251 | 252 | default: 253 | EmptyView() 254 | } 255 | } 256 | .id(999999) 257 | } 258 | .padding(.horizontal) 259 | .border(.green) 260 | .animation(.default, value: container.data) 261 | } 262 | .border(.red) 263 | } 264 | 265 | 266 | } 267 | 268 | struct DragRelocateDelegate: DropDelegate { 269 | let item: Model 270 | @Binding var listData: [Model] 271 | @Binding var current: Model? 272 | 273 | func dropEntered(info: DropInfo) { 274 | print("Drop entered: \(info); \(item); \(current) ") 275 | if item != current { 276 | let from = listData.firstIndex(of: current!)! 277 | let to = listData.firstIndex(of: item)! 278 | if listData[to].id != current!.id { 279 | listData.move(fromOffsets: IndexSet(integer: from), 280 | toOffset: to > from ? to + 1 : to) 281 | } 282 | } 283 | } 284 | 285 | func dropUpdated(info: DropInfo) -> DropProposal? { 286 | return DropProposal(operation: .move) 287 | } 288 | 289 | func performDrop(info: DropInfo) -> Bool { 290 | self.current = nil 291 | return true 292 | } 293 | } 294 | 295 | 296 | struct ProbeFoo_Preview: PreviewProvider { 297 | static var previews: some View { 298 | NavigationStack { 299 | 300 | ContentView() 301 | .navigationTitle("Demo") 302 | } 303 | } 304 | } 305 | 306 | struct GridCell: View { 307 | 308 | let model: Model 309 | 310 | var body: some View { 311 | HStack { 312 | Text(model.value.formatted()) 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-SwiftUI/InteractiveGridApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct InteractiveGridApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | NavigationStack { 8 | ContentView() 9 | .navigationTitle("Interactive Grid Demo") 10 | .navigationBarTitleDisplayMode(.inline) 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-SwiftUI/InteractiveGrid_SwiftUI.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-SwiftUI/Model.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Model: Hashable, Identifiable { 4 | enum Style { 5 | case compact 6 | case regular 7 | } 8 | 9 | var id: Int { 10 | return value 11 | } 12 | 13 | let value: Int 14 | let style: Style 15 | let allowsContextMenu: Bool 16 | } 17 | 18 | extension Model.Style: CustomStringConvertible { 19 | 20 | var description: String { 21 | switch self { 22 | case .compact: 23 | return "Compact" 24 | case .regular: 25 | return "Regular" 26 | } 27 | } 28 | 29 | var symbolName: String { 30 | switch self { 31 | case .compact: 32 | return "plus.square" 33 | case .regular: 34 | return "plus.rectangle" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-SwiftUI/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-UIKit/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-UIKit/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-UIKit/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-UIKit/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ContentView: View { 4 | 5 | @State 6 | private var models: [Model] = Model.makeAllCompact() 7 | 8 | @State 9 | private var shouldEnableContextMenu = true 10 | 11 | var body: some View { 12 | ZStack { 13 | LinearGradient(colors: [.mint, .orange, .blue], startPoint: .topLeading, endPoint: .bottomTrailing) 14 | .overlay(.ultraThinMaterial) 15 | 16 | InteractiveGridViewControllerRepresentable(models: models) 17 | } 18 | .edgesIgnoringSafeArea(.all) 19 | .toolbar { 20 | ToolbarItem { 21 | Menu("Demos") { 22 | Button("All Compact") { 23 | models = Model.makeAllCompact() 24 | } 25 | 26 | Button("All Regular") { 27 | models = Model.makeAllRegular() 28 | } 29 | 30 | Button("Mix-n-match") { 31 | models = Model.makeMixNMatch() 32 | } 33 | 34 | Button("Random") { 35 | models = Model.makeRandom() 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | 43 | extension Model { 44 | 45 | static func makeAllCompact() -> [Model] { 46 | return [ 47 | Model(value: 1, style: .compact, allowsContextMenu: true), 48 | Model(value: 2, style: .compact, allowsContextMenu: true), 49 | Model(value: 3, style: .compact, allowsContextMenu: true), 50 | Model(value: 4, style: .compact, allowsContextMenu: true), 51 | Model(value: 5, style: .compact, allowsContextMenu: true), 52 | Model(value: 6, style: .compact, allowsContextMenu: true) 53 | ] 54 | } 55 | 56 | static func makeAllRegular() -> [Model] { 57 | return [ 58 | Model(value: 1, style: .regular, allowsContextMenu: false), 59 | Model(value: 2, style: .regular, allowsContextMenu: false), 60 | Model(value: 3, style: .regular, allowsContextMenu: false), 61 | Model(value: 4, style: .regular, allowsContextMenu: false), 62 | Model(value: 5, style: .regular, allowsContextMenu: false), 63 | Model(value: 6, style: .regular, allowsContextMenu: false) 64 | ] 65 | } 66 | 67 | static func makeMixNMatch() -> [Model] { 68 | return [ 69 | Model(value: 1, style: .regular, allowsContextMenu: true), 70 | Model(value: 2, style: .compact, allowsContextMenu: true), 71 | Model(value: 3, style: .compact, allowsContextMenu: true), 72 | Model(value: 4, style: .regular, allowsContextMenu: true), 73 | Model(value: 5, style: .compact, allowsContextMenu: true), 74 | Model(value: 6, style: .compact, allowsContextMenu: true), 75 | Model(value: 7, style: .regular, allowsContextMenu: true), 76 | Model(value: 8, style: .compact, allowsContextMenu: true), 77 | Model(value: 9, style: .compact, allowsContextMenu: true), 78 | Model(value: 10, style: .regular, allowsContextMenu: true) 79 | ] 80 | } 81 | 82 | static func makeRandom() -> [Model] { 83 | let compactModels = (0.. NSCollectionLayoutGroup? { 25 | 26 | // Determining how to layout a cell requires knowing the 27 | // - previous cell's style (may be `nil`; first cell) 28 | // - current cell's style (non-`nil`) 29 | // - next cell's style (may be `nil`; last cell) 30 | 31 | // Special case if we are at the end. 32 | guard let nextStyle = nextStyle else { 33 | switch currentStyle { 34 | case .compact: 35 | return compactOrphanGroup 36 | case .regular: 37 | return regularGroup 38 | } 39 | } 40 | 41 | // This is the logic that drives how the compact and regular cells 42 | // are arranged in a 2-column layout. 43 | // 44 | // Here are the general row configurations: 45 | // | Compact | Compact | 46 | // | Compact | | 47 | // | R e g u l a r | 48 | // 49 | // A simple switch statement makes it super simple to 50 | // derive one of the three row configurations. 51 | // 52 | switch (previousStyle, currentStyle, nextStyle) { 53 | case (.none, .compact, .compact): 54 | return compactGroup 55 | case (.none, .compact, .regular): 56 | return compactOrphanGroup 57 | case (.compact, .compact, .compact): 58 | return nil 59 | case (.compact, .compact, .regular): 60 | return nil 61 | case (.regular, .compact, .compact): 62 | return compactGroup 63 | case (.regular, .compact, .regular): 64 | return compactOrphanGroup 65 | case (_, .regular, _): 66 | return regularGroup 67 | } 68 | } 69 | } 70 | 71 | extension DefaultDynamicLayoutGroupProvider { 72 | 73 | private func lazyCompactGroup() -> NSCollectionLayoutGroup { 74 | let compactGroup = NSCollectionLayoutGroup.horizontal( 75 | layoutSize: compactGroupSize, 76 | repeatingSubitem: compactWidthLayoutItem, 77 | count: 2 78 | ) 79 | compactGroup.interItemSpacing = .fixed(16) 80 | 81 | return compactGroup 82 | } 83 | 84 | private func lazyCompactOrphanGroup() -> NSCollectionLayoutGroup { 85 | return NSCollectionLayoutGroup.horizontal( 86 | layoutSize: compactOrphanGroupSize, 87 | repeatingSubitem: fullWidthLayoutItem, 88 | count: 1 89 | ) 90 | } 91 | 92 | private func lazyRegularGroup() -> NSCollectionLayoutGroup { 93 | return NSCollectionLayoutGroup.horizontal( 94 | layoutSize: regularGroupSize, 95 | repeatingSubitem: fullWidthLayoutItem, 96 | count: 1 97 | ) 98 | } 99 | } 100 | 101 | extension DefaultDynamicLayoutGroupProvider { 102 | 103 | private func lazyCompactWidthLayoutItem() -> NSCollectionLayoutItem { 104 | let layoutSize = NSCollectionLayoutSize( 105 | widthDimension: .fractionalWidth(0.5), 106 | heightDimension: .fractionalHeight(1.0) 107 | ) 108 | 109 | return NSCollectionLayoutItem(layoutSize: layoutSize) 110 | } 111 | 112 | private func lazyFullWidthLayoutItem() -> NSCollectionLayoutItem { 113 | let layoutSize = NSCollectionLayoutSize( 114 | widthDimension: .fractionalWidth(1.0), 115 | heightDimension: .fractionalHeight(1.0) 116 | ) 117 | 118 | return NSCollectionLayoutItem(layoutSize: layoutSize) 119 | } 120 | } 121 | 122 | extension DefaultDynamicLayoutGroupProvider { 123 | 124 | private func lazyCompactGroupSize() -> NSCollectionLayoutSize { 125 | return NSCollectionLayoutSize( 126 | widthDimension: .fractionalWidth(1.0), 127 | heightDimension: .fractionalWidth(0.5) 128 | ) 129 | } 130 | 131 | private func lazyCompactOrphanGroupSize() -> NSCollectionLayoutSize { 132 | return NSCollectionLayoutSize( 133 | widthDimension: .fractionalWidth(0.5), 134 | heightDimension: .fractionalWidth(0.5) 135 | ) 136 | } 137 | 138 | private func lazyRegularGroupSize() -> NSCollectionLayoutSize { 139 | return NSCollectionLayoutSize( 140 | widthDimension: .fractionalWidth(1.0), 141 | heightDimension: .fractionalWidth(0.5) 142 | ) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-UIKit/DynamicLayoutGroupProvider.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// This protocol declares an API for deriving a `NSCollectionLayoutGroup` based on 4 | /// the previous model style (may be `nil`), the current style (non-`nil`) and the next style (may be `nil`). 5 | protocol DynamicLayoutGroupProvider { 6 | 7 | func deriveLayoutGroup( 8 | basedOnPreviousStyle previousStyle: Model.Style?, 9 | currentStyle: Model.Style, 10 | nextStyle: Model.Style? 11 | ) -> NSCollectionLayoutGroup? 12 | } 13 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-UIKit/GridCell.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct GridCell: View { 4 | 5 | let model: Model 6 | 7 | var body: some View { 8 | VStack { 9 | Text(model.value.formatted()) 10 | .font(.headline) 11 | 12 | Text(model.allowsContextMenu ? "long press for menu" : "no menu") 13 | .multilineTextAlignment(.center) 14 | .font(.subheadline) 15 | } 16 | } 17 | } 18 | 19 | struct GridCell_Previews: PreviewProvider { 20 | static var previews: some View { 21 | GridCell(model: Model(value: 9, style: .regular, allowsContextMenu: true)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-UIKit/GridCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | /// A simple cell used to display `Model`s in the `InteractiveGridViewController`. 5 | /// 6 | /// The `dragStateDidChange(_:)` method is overridden to alter the opacity of the 7 | /// cell during drag state changes. 8 | final class GridCollectionViewCell: UICollectionViewCell { 9 | 10 | private(set) lazy var label = lazyLabel() 11 | private(set) lazy var subtitle = lazySubtitle() 12 | 13 | override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | 16 | contentView.backgroundColor = .tertiarySystemBackground 17 | contentView.layer.cornerRadius = 16 18 | } 19 | 20 | required init?(coder: NSCoder) { 21 | fatalError("not implemented") 22 | } 23 | 24 | override func dragStateDidChange(_ dragState: UICollectionViewCell.DragState) { 25 | 26 | // The docs say to call super to apply default behavior, but the 27 | // default behavior is not documented. Therefore, let's call super anyway 28 | // just to be sure we are not missing out on something. 29 | // 30 | // Note: Omitting this call appears to have zero negative impact. 31 | super.dragStateDidChange(dragState) 32 | 33 | // You'll notice that the cell's layer, not the `contentView`'s layer's opacity, 34 | // is changed. We need to affect the cell's opacity in order to improve the 35 | // drag/drop visual behavior we are looking to achieve. Specifically, when the drag 36 | // begins, we want this cell to disappear to make it look like we actually picked 37 | // up the view. If this is not done, then you'll see the lifted preview and this cell. 38 | // 39 | // Seems like this is something that `UICollectionView` should take care of automatically. 40 | // Perhaps it does, but I have yet to find the hook. 41 | switch dragState { 42 | case .lifting, .dragging: 43 | layer.opacity = 0 44 | case .none: 45 | layer.opacity = 1 46 | @unknown default: 47 | layer.opacity = 1 48 | } 49 | } 50 | } 51 | 52 | extension GridCollectionViewCell { 53 | 54 | private func lazyLabel() -> UILabel { 55 | let label = UILabel() 56 | label.translatesAutoresizingMaskIntoConstraints = false 57 | 58 | label.font = .preferredFont(forTextStyle: .largeTitle) 59 | 60 | contentView.addSubview(label) 61 | NSLayoutConstraint.activate([ 62 | label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), 63 | label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) 64 | ]) 65 | 66 | return label 67 | } 68 | 69 | private func lazySubtitle() -> UILabel { 70 | let subtitle = UILabel() 71 | subtitle.translatesAutoresizingMaskIntoConstraints = false 72 | 73 | subtitle.font = .preferredFont(forTextStyle: .footnote) 74 | 75 | contentView.addSubview(subtitle) 76 | NSLayoutConstraint.activate([ 77 | subtitle.centerXAnchor.constraint(equalTo: label.centerXAnchor), 78 | subtitle.topAnchor.constraint(equalTo: label.bottomAnchor) 79 | ]) 80 | 81 | return subtitle 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-UIKit/InteractiveGridApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct InteractiveGridApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | NavigationStack { 8 | ContentView() 9 | .navigationTitle("Interactive Grid Demo") 10 | .navigationBarTitleDisplayMode(.inline) 11 | } 12 | .navigationViewStyle(.stack) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-UIKit/InteractiveGridViewController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | import SwiftUI 4 | 5 | /// This view controller presents a `UICollectionView` driven by an array of `Model`s, where each 6 | /// `Model` provides an `Int` value and a `Model.Style` (compact or regular). 7 | /// 8 | /// The goal is to use stock `UICollectionView` API to build a re-orderable grid view of cells backed by 9 | /// a "model" that supports displaying "compact" and "regular" content. 10 | /// 11 | /// Stock API that you'll see here: 12 | /// - `UICollectionViewDiffableDataSource` 13 | /// - Compositional layout APIs (e.g. `NSCollectionLayoutGroup`) 14 | /// - `UIContextMenuConfiguration` for presenting a context menu on a cell 15 | /// 16 | /// Honestly, putting this together was a like a crazy game of whack-a-mole. There are so many ways to do 17 | /// the same thing that it feels like nothing quite works as expected. For example, `UICollectionView` 18 | /// has the ability to re-order elements, but that doesn't work directly with a diffable data source. Instead 19 | /// the diffable data source takes control of reordering. But... then the cells don't seem to know that a reorder 20 | /// is in flight. 21 | /// 22 | /// There are various notes sprinkled throughout this implementation about things that confused me or that I think are just 23 | /// downright broken. 24 | /// 25 | /// The end result is a collection view that works about 95% percent of the time as expected by just using the combination 26 | /// of stock UIKit API. 27 | final class InteractiveGridViewController: UIViewController { 28 | 29 | /// Mutable collection of `Model`s. 30 | var models: [Model] { 31 | get { 32 | return dataSource.snapshot().itemIdentifiers 33 | } 34 | 35 | set { 36 | var snapshot = dataSource.snapshot() 37 | snapshot.deleteAllItems() 38 | 39 | if newValue.count > 0 { 40 | snapshot.appendSections([.main]) 41 | snapshot.appendItems(newValue, toSection: .main) 42 | } 43 | 44 | dataSource.apply(snapshot) 45 | } 46 | } 47 | 48 | private lazy var dataSource = lazyDataSource() 49 | private lazy var collectionView = lazyCollectionView() 50 | 51 | private lazy var layoutGroupProvider = lazyDynamicLayoutGroupProvider() 52 | 53 | // Represents a snapshot of how the items/ cells will be placed when 54 | // the reorder drag operation completes. This snapshot is used to dynamically 55 | // layout the items/ cells based on their style (compact or regular). 56 | private var proposedDragItems: [Model]? = nil 57 | } 58 | 59 | // MARK: View Life Cycle 60 | 61 | extension InteractiveGridViewController { 62 | 63 | override func viewDidLoad() { 64 | super.viewDidLoad() 65 | 66 | enableReorderSupport(on: dataSource) 67 | 68 | add(collectionView, to: view) 69 | configure(collectionView) 70 | } 71 | } 72 | 73 | // MARK: UICollectionViewDragDelegate 74 | 75 | extension InteractiveGridViewController: UICollectionViewDragDelegate { 76 | 77 | func collectionView( 78 | _: UICollectionView, 79 | itemsForBeginning _: UIDragSession, 80 | at _: IndexPath 81 | ) -> [UIDragItem] { 82 | 83 | // There really is not any reason to clear this here because the method below will constantly 84 | // update this property. It's here just to document that no `proposedDragItems` should exist 85 | // when a drag begins. 86 | proposedDragItems = nil 87 | 88 | // Point of confusion: 89 | // 90 | // The data source handles setting up the drag item via its reordering 91 | // support via the diffable data source. 92 | // 93 | // This method is a required method on `UICollectionViewDragDelegate`. It simply returns an empty 94 | // array of `UIDragItem`s, and lets the diffable data source drive the drag items. 95 | return [] 96 | } 97 | 98 | func collectionView( 99 | _: UICollectionView, 100 | targetIndexPathForMoveOfItemFromOriginalIndexPath originalIndexPath: IndexPath, 101 | atCurrentIndexPath currentIndexPath: IndexPath, 102 | toProposedIndexPath proposedIndexPath: IndexPath 103 | ) -> IndexPath { 104 | 105 | let currentItem = dataSource.itemIdentifier(for: currentIndexPath) 106 | let proposedItem = dataSource.itemIdentifier(for: proposedIndexPath) 107 | 108 | // Point of confusion: 109 | // 110 | // I guess it's fine that `UICollectionView` constantly calls this method even if the target index 111 | // path hasn't actually changed. This guard here ensures that we are not doing more work than needed 112 | // when the user is just dragging over the same index path. 113 | guard currentItem != proposedItem else { 114 | return proposedIndexPath 115 | } 116 | 117 | // Next is where we build an array of `Model`s that represent the current drag state. 118 | // The `proposedDragItems` allows us to dynamically layout the cells based on a derived 119 | // compositional layout. 120 | 121 | let from = originalIndexPath.item 122 | let to = proposedIndexPath.item 123 | 124 | let fromOffsets = IndexSet(integer: from) 125 | let toOffset = to > from ? to + 1 : to 126 | 127 | // We now take a snapshot and adjust the items as needed. 128 | // The mutated snapshot is _not_ applied to the data source. Instead 129 | // the snapshot is stored in the `proposedDragItems`. The `proposedDragItems` 130 | // is used to drive dynamic changes to the compositional layout during a drag/ reorder. 131 | var proposedDragItems = dataSource.snapshot().itemIdentifiers 132 | proposedDragItems.move(fromOffsets: fromOffsets, toOffset: toOffset) 133 | self.proposedDragItems = proposedDragItems 134 | 135 | return proposedIndexPath 136 | } 137 | 138 | func collectionView( 139 | _ collectionView: UICollectionView, 140 | dragPreviewParametersForItemAt indexPath: IndexPath 141 | ) -> UIDragPreviewParameters? { 142 | 143 | // See the comments in the private `makePreviewParameters(forCell:)` method for 144 | // more details. 145 | return previewParameters(forItemAt: indexPath, collectionView: collectionView) 146 | } 147 | } 148 | 149 | // MARK: UICollectionViewDropDelegate 150 | 151 | extension InteractiveGridViewController: UICollectionViewDropDelegate { 152 | 153 | func collectionView(_: UICollectionView, performDropWith _: UICollectionViewDropCoordinator) { 154 | // Note: This is a required `UICollectionViewDropDelegate` method. 155 | } 156 | 157 | func collectionView( 158 | _ collectionView: UICollectionView, 159 | dropPreviewParametersForItemAt indexPath: IndexPath 160 | ) -> UIDragPreviewParameters? { 161 | 162 | // See the comments in the private `makePreviewParameters(forCell:)` method for more details. 163 | return previewParameters(forItemAt: indexPath, collectionView: collectionView) 164 | } 165 | } 166 | 167 | // MARK: UICollectionViewDelegate - Menu Support 168 | 169 | extension InteractiveGridViewController: UICollectionViewDelegate { 170 | 171 | func collectionView( 172 | _: UICollectionView, 173 | contextMenuConfigurationForItemAt indexPath: IndexPath, 174 | point _: CGPoint 175 | ) -> UIContextMenuConfiguration? { 176 | 177 | guard let item = dataSource.itemIdentifier(for: indexPath) else { 178 | return nil 179 | } 180 | 181 | // Point of confusion: 182 | // 183 | // The good thing is that I really want a context menu to appear when the user long 184 | // presses on a cell. 185 | // 186 | // Here's where things get confusing. For some reason the `UICollectionView` really 187 | // needs this delegate method to be implemented and to return a non-`nil` 188 | // `UIContextMenuConfiguration` (even if the there are not any menus). If this is not 189 | // implemented then, when lifting the cell to start a drag operation, the cell becomes 190 | // transparent (not what I want). 191 | // 192 | // So why does the cell become transparent? It's because the `GridCell` tracks the 193 | // "lift" and "dragging" state. This is where the game of whack-a-mole continues. 194 | // The `UICollectionView`'s reordering support does "lift" the selected view, which looks 195 | // nice, but there's a remnant of the view that stays around until the user drags 196 | // far enough to warrant the collection view to hide/ remove this remnant view. 197 | // To compensate for this, the `GridCell` sets the cell's content view's opacity to 198 | // zero during a "lift" and "drag". 199 | // 200 | // For some reason, if a non-`nil` `UIContextMenuConfiguration` is returned, then the 201 | // "lifted" cell remains visible (i.e. the lifted view's opacity is carried forward to 202 | // "lifted" cell. 203 | // 204 | // In summary... to get things to work with the lift and drag, a non-`nil` `UIContextMenuConfiguration` 205 | // is always returned but it may not actually have a `UIMenu` to display. 206 | // 207 | // See `makeContextMenu(item:)`. 208 | 209 | return UIContextMenuConfiguration( 210 | identifier: indexPath as NSIndexPath, 211 | previewProvider: nil, 212 | actionProvider: { [weak self] _ in 213 | return self?.makeContextMenu(item: item) 214 | } 215 | ) 216 | } 217 | 218 | func collectionView( 219 | _ collectionView: UICollectionView, 220 | previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration 221 | ) -> UITargetedPreview? { 222 | 223 | return makeTargetedPreview(withConfiguration: configuration, collectionView: collectionView) 224 | } 225 | 226 | func collectionView( 227 | _ collectionView: UICollectionView, 228 | previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration 229 | ) -> UITargetedPreview? { 230 | 231 | return makeTargetedPreview(withConfiguration: configuration, collectionView: collectionView) 232 | } 233 | 234 | private func makeTargetedPreview( 235 | withConfiguration configuration: UIContextMenuConfiguration, 236 | collectionView: UICollectionView 237 | ) -> UITargetedPreview? { 238 | 239 | guard let indexPath = configuration.identifier as? IndexPath else { 240 | return nil 241 | } 242 | 243 | guard let cell = collectionView.cellForItem(at: indexPath) else { 244 | return nil 245 | } 246 | 247 | return UITargetedPreview(view: cell, parameters: makePreviewParameters(forCell: cell)) 248 | } 249 | 250 | private func makeContextMenu(item: Model) -> UIMenu? { 251 | 252 | // Point of confusion: The collection view appears to render the lifted view correctly 253 | // only if there is a menu configuration (even if that menu doesn't have any actions). 254 | // 255 | // The `UIContextMenuConfiguration` appears to be fine with a `nil` `UIMenu`, which 256 | // is good because this seems to fix a `UICollectionView` "lift preview" bug. 257 | guard item.allowsContextMenu else { 258 | return nil 259 | } 260 | 261 | return UIMenu(children: [ 262 | makeActionToAddCompactItem(afterItem: item, in: dataSource), 263 | makeActionToAddRegularItem(afterItem: item, in: dataSource), 264 | makeAction(toRemove: item, from: dataSource) 265 | ]) 266 | } 267 | 268 | private func makeActionToAddModel( 269 | withStyle style: Model.Style, 270 | afterItem: Model, 271 | in dataSource: UICollectionViewDiffableDataSource 272 | ) -> UIAction { 273 | 274 | return UIAction( 275 | title: "Add \(style.description) After", 276 | image: UIImage(systemName: style.symbolName), 277 | attributes: [] 278 | ) { [dataSource] _ in 279 | 280 | var snapshot = dataSource.snapshot() 281 | 282 | let model = Model(value: snapshot.numberOfItems + 1, style: style, allowsContextMenu: true) 283 | snapshot.insertItems([model], afterItem: afterItem) 284 | dataSource.apply(snapshot) 285 | } 286 | } 287 | 288 | private func makeActionToAddCompactItem( 289 | afterItem: Model, 290 | in dataSource: UICollectionViewDiffableDataSource 291 | ) -> UIAction { 292 | 293 | return makeActionToAddModel(withStyle: .compact, afterItem: afterItem, in: dataSource) 294 | } 295 | 296 | private func makeActionToAddRegularItem( 297 | afterItem: Model, 298 | in dataSource: UICollectionViewDiffableDataSource 299 | ) -> UIAction { 300 | 301 | return makeActionToAddModel(withStyle: .regular, afterItem: afterItem, in: dataSource) 302 | } 303 | 304 | private func makeAction( 305 | toRemove item: Model, 306 | from dataSource: UICollectionViewDiffableDataSource 307 | ) -> UIAction { 308 | 309 | return UIAction( 310 | title: "Remove", 311 | image: UIImage(systemName: "trash"), 312 | attributes: .destructive 313 | ) { [dataSource] _ in 314 | 315 | var snapshot = dataSource.snapshot() 316 | snapshot.deleteItems([item]) 317 | dataSource.applySnapshotUsingReloadData(snapshot) 318 | } 319 | } 320 | } 321 | 322 | // MARK: Private Helpers To Create `UIPreviewParameters` (lift, drag, and drop) 323 | 324 | extension InteractiveGridViewController { 325 | 326 | private func previewParameters( 327 | forItemAt indexPath: IndexPath, 328 | collectionView: UICollectionView 329 | ) -> UIDragPreviewParameters? { 330 | 331 | guard let cell = collectionView.cellForItem(at: indexPath) else { 332 | return nil 333 | } 334 | 335 | return makePreviewParameters(forCell: cell) 336 | } 337 | 338 | private func makePreviewParameters(forCell cell: UICollectionViewCell) -> P { 339 | 340 | // The `GridCell` uses a rounded rectangle. As of iOS 15, we must tell the collection view 341 | // how to render the preview by the setting the `shadowPath` and `visiblePath`. 342 | // 343 | // If this is not done then the preview view will not show rounded corners when the view 344 | // is lifted, dragged, and eventually dropped. 345 | 346 | let previewParameters = P() 347 | previewParameters.shadowPath = UIBezierPath(roundedRect: cell.bounds, cornerRadius: cell.contentView.layer.cornerRadius) 348 | previewParameters.visiblePath = previewParameters.shadowPath 349 | previewParameters.backgroundColor = .clear 350 | 351 | return previewParameters 352 | } 353 | } 354 | 355 | // MARK: Compositional Layout Set Up 356 | 357 | extension InteractiveGridViewController { 358 | 359 | private func makeDynamicCollectionViewLayout() -> UICollectionViewLayout { 360 | 361 | // Our design requires dynamically adjusting the layout based on the current 362 | // array of `Model`s. Therefore, we use the `UICollectionViewCompositionalLayout/sectionProvider` 363 | // API to dynamically generate the layout based on the `proposedDragItems` 364 | // (if dragging) or the diffable data source (if not dragging). 365 | // 366 | // This method is called a lot during dragging. The collection view seems to 367 | // quickly handle changes without hitches. 368 | 369 | let configuration = UICollectionViewCompositionalLayoutConfiguration() 370 | configuration.contentInsetsReference = .readableContent 371 | 372 | return UICollectionViewCompositionalLayout(sectionProvider: { [weak self] _, _ in 373 | guard let self = self else { 374 | return nil 375 | } 376 | 377 | // This is the where the magic happens to get the collection view cells to move 378 | // around as the user drags a lifted cell. 379 | let items = self.proposedDragItems ?? self.dataSource.snapshot().itemIdentifiers 380 | let styles = items.map { $0.style } 381 | if styles.isEmpty { 382 | return nil 383 | } 384 | return self.makeSection(for: styles) 385 | }, configuration: configuration) 386 | } 387 | 388 | private func makeSection(for styles: [Model.Style]) -> NSCollectionLayoutSection { 389 | 390 | var groups: [NSCollectionLayoutItem] = [] 391 | 392 | var previousStyle: Model.Style? = nil 393 | for index in 0.. NSCollectionLayoutSection { 421 | let section = NSCollectionLayoutSection(group: group) 422 | section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 10, trailing: 16) 423 | 424 | return section 425 | } 426 | 427 | private func makeOuterGroup(forSubitems subitems: [NSCollectionLayoutItem]) -> NSCollectionLayoutGroup { 428 | let outerGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(1)) 429 | let outerGroup = NSCollectionLayoutGroup.vertical(layoutSize: outerGroupSize, subitems: subitems) 430 | outerGroup.interItemSpacing = .fixed(16) 431 | 432 | return outerGroup 433 | } 434 | 435 | private func lazyDynamicLayoutGroupProvider() -> DynamicLayoutGroupProvider { 436 | return DefaultDynamicLayoutGroupProvider() 437 | } 438 | } 439 | 440 | // MARK: Diffable Data Source Set Up 441 | 442 | extension InteractiveGridViewController { 443 | 444 | private func lazyDataSource() -> UICollectionViewDiffableDataSource { 445 | 446 | let cellRegistration = makeCellRegistration() 447 | return UICollectionViewDiffableDataSource(collectionView: collectionView) { (collectionView, indexPath, identifier) in 448 | return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier) 449 | } 450 | } 451 | 452 | private func makeCellRegistration() -> UICollectionView.CellRegistration { 453 | return UICollectionView.CellRegistration { (cell, indexPath, model) in 454 | cell.contentConfiguration = UIHostingConfiguration { 455 | GridCell(model: model) 456 | } 457 | .background { 458 | RoundedRectangle(cornerRadius: 16) 459 | .fill(Color(uiColor: .systemBackground)) 460 | } 461 | 462 | // The cell's contentView `CALayer` corner radius is set to the same radius as the background to ensure 463 | // that the "drag preview" background does not show through. 464 | cell.contentView.layer.cornerRadius = 16 465 | } 466 | } 467 | } 468 | 469 | // MARK: UICollectionView Set Up 470 | 471 | extension InteractiveGridViewController { 472 | 473 | private func lazyCollectionView() -> UICollectionView { 474 | return UICollectionView(frame: .zero, collectionViewLayout: makeDynamicCollectionViewLayout()) 475 | } 476 | 477 | private func add(_ collectionView: UICollectionView, to view: UIView) { 478 | collectionView.translatesAutoresizingMaskIntoConstraints = false 479 | collectionView.backgroundColor = .clear 480 | 481 | view.addSubview(collectionView) 482 | 483 | NSLayoutConstraint.activate([ 484 | collectionView.topAnchor.constraint(equalTo: view.topAnchor), 485 | collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 486 | collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), 487 | collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) 488 | ]) 489 | } 490 | 491 | private func configure(_ collectionView: UICollectionView) { 492 | collectionView.backgroundColor = .clear 493 | 494 | collectionView.delegate = self 495 | collectionView.dragDelegate = self 496 | collectionView.dropDelegate = self 497 | } 498 | } 499 | 500 | // MARK: Diffable Data Source Reorder Support 501 | 502 | extension InteractiveGridViewController { 503 | 504 | private func enableReorderSupport(on dataSource: UICollectionViewDiffableDataSource) { 505 | dataSource.reorderingHandlers.canReorderItem = { _ in 506 | // All items are eligible for reordering. 507 | return true 508 | } 509 | 510 | dataSource.reorderingHandlers.didReorder = { [weak self] value in 511 | 512 | // Point of confusion: 513 | // 514 | // If this callback is not set, then the data source appears to 515 | // ignore the drop and puts the items back in the pre-drag state. 516 | // Bug? I don't know. There's no documentation. 517 | 518 | // Now that the drag is finished, the `proposedDragItems` are cleared. 519 | // New items are set when the user starts dragging again. 520 | self?.proposedDragItems = nil 521 | } 522 | } 523 | } 524 | 525 | extension InteractiveGridViewController { 526 | 527 | private enum Section { 528 | case main 529 | } 530 | } 531 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-UIKit/InteractiveGridViewControllerRepresentable.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// A very simple SwiftUI wrapper around the `InteractiveGridViewController`. 4 | /// 5 | /// The `updateUIViewController(_:context:)` method executes when the user selects 6 | /// a new set of `Model`s from the demo app's "select model" menu. 7 | struct InteractiveGridViewControllerRepresentable: UIViewControllerRepresentable { 8 | 9 | var models: [Model] 10 | 11 | func makeUIViewController(context _: Context) -> InteractiveGridViewController { 12 | return InteractiveGridViewController() 13 | } 14 | 15 | func updateUIViewController(_ viewController: InteractiveGridViewController, context _: Context) { 16 | viewController.models = models 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-UIKit/Model.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Model: Hashable { 4 | enum Style { 5 | case compact 6 | case regular 7 | } 8 | 9 | let value: Int 10 | let style: Style 11 | let allowsContextMenu: Bool 12 | } 13 | 14 | extension Model.Style: CustomStringConvertible { 15 | 16 | var description: String { 17 | switch self { 18 | case .compact: 19 | return "Compact" 20 | case .regular: 21 | return "Regular" 22 | } 23 | } 24 | 25 | var symbolName: String { 26 | switch self { 27 | case .compact: 28 | return "plus.square" 29 | case .regular: 30 | return "plus.rectangle" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid-UIKit/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5D07C92A286F20B0009DE34B /* InteractiveGridApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D07C929286F20B0009DE34B /* InteractiveGridApp.swift */; }; 11 | 5D07C92C286F20B0009DE34B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D07C92B286F20B0009DE34B /* ContentView.swift */; }; 12 | 5D07C92E286F20B0009DE34B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5D07C92D286F20B0009DE34B /* Assets.xcassets */; }; 13 | 5D07C932286F20B0009DE34B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5D07C931286F20B0009DE34B /* Preview Assets.xcassets */; }; 14 | 5D07C937286F2476009DE34B /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D07C936286F2476009DE34B /* Model.swift */; }; 15 | 5D07C93B286F2810009DE34B /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 5D07C93A286F2810009DE34B /* Collections */; }; 16 | 5D42D9F4270BDF350055D1E4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5D42D9F3270BDF350055D1E4 /* Assets.xcassets */; }; 17 | 5D42D9F7270BDF350055D1E4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5D42D9F6270BDF350055D1E4 /* Preview Assets.xcassets */; }; 18 | 5D42DA13270BE00D0055D1E4 /* DefaultDynamicLayoutGroupProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D42DA0D270BE00C0055D1E4 /* DefaultDynamicLayoutGroupProvider.swift */; }; 19 | 5D42DA14270BE00D0055D1E4 /* DynamicLayoutGroupProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D42DA0E270BE00D0055D1E4 /* DynamicLayoutGroupProvider.swift */; }; 20 | 5D42DA15270BE00D0055D1E4 /* InteractiveGridViewControllerRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D42DA0F270BE00D0055D1E4 /* InteractiveGridViewControllerRepresentable.swift */; }; 21 | 5D42DA16270BE00D0055D1E4 /* InteractiveGridViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D42DA10270BE00D0055D1E4 /* InteractiveGridViewController.swift */; }; 22 | 5D42DA17270BE00D0055D1E4 /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D42DA11270BE00D0055D1E4 /* Model.swift */; }; 23 | 5D42DA18270BE00D0055D1E4 /* GridCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D42DA12270BE00D0055D1E4 /* GridCollectionViewCell.swift */; }; 24 | 5D42DA1C270BE0440055D1E4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D42DA1B270BE0440055D1E4 /* ContentView.swift */; }; 25 | 5D42DA1E270BE0560055D1E4 /* InteractiveGridApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D42DA1D270BE0560055D1E4 /* InteractiveGridApp.swift */; }; 26 | 5D6FD1E9286F6F6C008F2487 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6FD1E8286F6F6C008F2487 /* main.swift */; }; 27 | 5D6FD1EF286F6FBD008F2487 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 5D6FD1EE286F6FBD008F2487 /* Collections */; }; 28 | 5D6FD1F2286F7001008F2487 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 5D6FD1F1286F7001008F2487 /* Algorithms */; }; 29 | 5D921DDE2853FF59007C0EE4 /* GridCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D921DDD2853FF59007C0EE4 /* GridCell.swift */; }; 30 | 5DAFABDC286F91BA00D72C07 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 5DAFABDB286F91BA00D72C07 /* Algorithms */; }; 31 | 5DDC6446270DDFDD0059AA6D /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 5DDC6445270DDFDD0059AA6D /* README.md */; }; 32 | /* End PBXBuildFile section */ 33 | 34 | /* Begin PBXCopyFilesBuildPhase section */ 35 | 5D6FD1E4286F6F6C008F2487 /* CopyFiles */ = { 36 | isa = PBXCopyFilesBuildPhase; 37 | buildActionMask = 2147483647; 38 | dstPath = /usr/share/man/man1/; 39 | dstSubfolderSpec = 0; 40 | files = ( 41 | ); 42 | runOnlyForDeploymentPostprocessing = 1; 43 | }; 44 | /* End PBXCopyFilesBuildPhase section */ 45 | 46 | /* Begin PBXFileReference section */ 47 | 5D07C927286F20B0009DE34B /* InteractiveGrid-SwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "InteractiveGrid-SwiftUI.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | 5D07C929286F20B0009DE34B /* InteractiveGridApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveGridApp.swift; sourceTree = ""; }; 49 | 5D07C92B286F20B0009DE34B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 50 | 5D07C92D286F20B0009DE34B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 51 | 5D07C92F286F20B0009DE34B /* InteractiveGrid_SwiftUI.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = InteractiveGrid_SwiftUI.entitlements; sourceTree = ""; }; 52 | 5D07C931286F20B0009DE34B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 53 | 5D07C936286F2476009DE34B /* Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Model.swift; sourceTree = ""; }; 54 | 5D42D9EC270BDF340055D1E4 /* InteractiveGrid-UIKit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "InteractiveGrid-UIKit.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 55 | 5D42D9F3270BDF350055D1E4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 56 | 5D42D9F6270BDF350055D1E4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 57 | 5D42DA0D270BE00C0055D1E4 /* DefaultDynamicLayoutGroupProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultDynamicLayoutGroupProvider.swift; sourceTree = ""; }; 58 | 5D42DA0E270BE00D0055D1E4 /* DynamicLayoutGroupProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DynamicLayoutGroupProvider.swift; sourceTree = ""; }; 59 | 5D42DA0F270BE00D0055D1E4 /* InteractiveGridViewControllerRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InteractiveGridViewControllerRepresentable.swift; sourceTree = ""; }; 60 | 5D42DA10270BE00D0055D1E4 /* InteractiveGridViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InteractiveGridViewController.swift; sourceTree = ""; }; 61 | 5D42DA11270BE00D0055D1E4 /* Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Model.swift; sourceTree = ""; }; 62 | 5D42DA12270BE00D0055D1E4 /* GridCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridCollectionViewCell.swift; sourceTree = ""; }; 63 | 5D42DA1B270BE0440055D1E4 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 64 | 5D42DA1D270BE0560055D1E4 /* InteractiveGridApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InteractiveGridApp.swift; sourceTree = ""; }; 65 | 5D6FD1E6286F6F6C008F2487 /* Playground */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = Playground; sourceTree = BUILT_PRODUCTS_DIR; }; 66 | 5D6FD1E8286F6F6C008F2487 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 67 | 5D921DDD2853FF59007C0EE4 /* GridCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridCell.swift; sourceTree = ""; }; 68 | 5DDC6445270DDFDD0059AA6D /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 69 | /* End PBXFileReference section */ 70 | 71 | /* Begin PBXFrameworksBuildPhase section */ 72 | 5D07C924286F20B0009DE34B /* Frameworks */ = { 73 | isa = PBXFrameworksBuildPhase; 74 | buildActionMask = 2147483647; 75 | files = ( 76 | 5DAFABDC286F91BA00D72C07 /* Algorithms in Frameworks */, 77 | ); 78 | runOnlyForDeploymentPostprocessing = 0; 79 | }; 80 | 5D42D9E9270BDF340055D1E4 /* Frameworks */ = { 81 | isa = PBXFrameworksBuildPhase; 82 | buildActionMask = 2147483647; 83 | files = ( 84 | 5D07C93B286F2810009DE34B /* Collections in Frameworks */, 85 | ); 86 | runOnlyForDeploymentPostprocessing = 0; 87 | }; 88 | 5D6FD1E3286F6F6C008F2487 /* Frameworks */ = { 89 | isa = PBXFrameworksBuildPhase; 90 | buildActionMask = 2147483647; 91 | files = ( 92 | 5D6FD1F2286F7001008F2487 /* Algorithms in Frameworks */, 93 | 5D6FD1EF286F6FBD008F2487 /* Collections in Frameworks */, 94 | ); 95 | runOnlyForDeploymentPostprocessing = 0; 96 | }; 97 | /* End PBXFrameworksBuildPhase section */ 98 | 99 | /* Begin PBXGroup section */ 100 | 5D07C928286F20B0009DE34B /* InteractiveGrid-SwiftUI */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 5D07C929286F20B0009DE34B /* InteractiveGridApp.swift */, 104 | 5D07C92B286F20B0009DE34B /* ContentView.swift */, 105 | 5D07C936286F2476009DE34B /* Model.swift */, 106 | 5D07C92D286F20B0009DE34B /* Assets.xcassets */, 107 | 5D07C92F286F20B0009DE34B /* InteractiveGrid_SwiftUI.entitlements */, 108 | 5D07C930286F20B0009DE34B /* Preview Content */, 109 | ); 110 | path = "InteractiveGrid-SwiftUI"; 111 | sourceTree = ""; 112 | }; 113 | 5D07C930286F20B0009DE34B /* Preview Content */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 5D07C931286F20B0009DE34B /* Preview Assets.xcassets */, 117 | ); 118 | path = "Preview Content"; 119 | sourceTree = ""; 120 | }; 121 | 5D42D9E3270BDF340055D1E4 = { 122 | isa = PBXGroup; 123 | children = ( 124 | 5DDC6445270DDFDD0059AA6D /* README.md */, 125 | 5D42D9EE270BDF340055D1E4 /* InteractiveGrid-UIKit */, 126 | 5D07C928286F20B0009DE34B /* InteractiveGrid-SwiftUI */, 127 | 5D6FD1E7286F6F6C008F2487 /* Playground */, 128 | 5D42D9ED270BDF340055D1E4 /* Products */, 129 | 5D6FD1ED286F6FBD008F2487 /* Frameworks */, 130 | ); 131 | sourceTree = ""; 132 | }; 133 | 5D42D9ED270BDF340055D1E4 /* Products */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | 5D42D9EC270BDF340055D1E4 /* InteractiveGrid-UIKit.app */, 137 | 5D07C927286F20B0009DE34B /* InteractiveGrid-SwiftUI.app */, 138 | 5D6FD1E6286F6F6C008F2487 /* Playground */, 139 | ); 140 | name = Products; 141 | sourceTree = ""; 142 | }; 143 | 5D42D9EE270BDF340055D1E4 /* InteractiveGrid-UIKit */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | 5D42DA1D270BE0560055D1E4 /* InteractiveGridApp.swift */, 147 | 5D42DA1B270BE0440055D1E4 /* ContentView.swift */, 148 | 5D42DA10270BE00D0055D1E4 /* InteractiveGridViewController.swift */, 149 | 5D42DA0F270BE00D0055D1E4 /* InteractiveGridViewControllerRepresentable.swift */, 150 | 5D42DA0E270BE00D0055D1E4 /* DynamicLayoutGroupProvider.swift */, 151 | 5D42DA0D270BE00C0055D1E4 /* DefaultDynamicLayoutGroupProvider.swift */, 152 | 5D42DA12270BE00D0055D1E4 /* GridCollectionViewCell.swift */, 153 | 5D921DDD2853FF59007C0EE4 /* GridCell.swift */, 154 | 5D42DA11270BE00D0055D1E4 /* Model.swift */, 155 | 5D42D9F3270BDF350055D1E4 /* Assets.xcassets */, 156 | 5D42D9F5270BDF350055D1E4 /* Preview Content */, 157 | ); 158 | path = "InteractiveGrid-UIKit"; 159 | sourceTree = ""; 160 | }; 161 | 5D42D9F5270BDF350055D1E4 /* Preview Content */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | 5D42D9F6270BDF350055D1E4 /* Preview Assets.xcassets */, 165 | ); 166 | path = "Preview Content"; 167 | sourceTree = ""; 168 | }; 169 | 5D6FD1E7286F6F6C008F2487 /* Playground */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | 5D6FD1E8286F6F6C008F2487 /* main.swift */, 173 | ); 174 | path = Playground; 175 | sourceTree = ""; 176 | }; 177 | 5D6FD1ED286F6FBD008F2487 /* Frameworks */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | ); 181 | name = Frameworks; 182 | sourceTree = ""; 183 | }; 184 | /* End PBXGroup section */ 185 | 186 | /* Begin PBXNativeTarget section */ 187 | 5D07C926286F20B0009DE34B /* InteractiveGrid-SwiftUI */ = { 188 | isa = PBXNativeTarget; 189 | buildConfigurationList = 5D07C933286F20B0009DE34B /* Build configuration list for PBXNativeTarget "InteractiveGrid-SwiftUI" */; 190 | buildPhases = ( 191 | 5D07C923286F20B0009DE34B /* Sources */, 192 | 5D07C924286F20B0009DE34B /* Frameworks */, 193 | 5D07C925286F20B0009DE34B /* Resources */, 194 | ); 195 | buildRules = ( 196 | ); 197 | dependencies = ( 198 | ); 199 | name = "InteractiveGrid-SwiftUI"; 200 | packageProductDependencies = ( 201 | 5DAFABDB286F91BA00D72C07 /* Algorithms */, 202 | ); 203 | productName = "InteractiveGrid-SwiftUI"; 204 | productReference = 5D07C927286F20B0009DE34B /* InteractiveGrid-SwiftUI.app */; 205 | productType = "com.apple.product-type.application"; 206 | }; 207 | 5D42D9EB270BDF340055D1E4 /* InteractiveGrid-UIKit */ = { 208 | isa = PBXNativeTarget; 209 | buildConfigurationList = 5D42D9FA270BDF350055D1E4 /* Build configuration list for PBXNativeTarget "InteractiveGrid-UIKit" */; 210 | buildPhases = ( 211 | 5D42D9E8270BDF340055D1E4 /* Sources */, 212 | 5D42D9E9270BDF340055D1E4 /* Frameworks */, 213 | 5D42D9EA270BDF340055D1E4 /* Resources */, 214 | ); 215 | buildRules = ( 216 | ); 217 | dependencies = ( 218 | ); 219 | name = "InteractiveGrid-UIKit"; 220 | packageProductDependencies = ( 221 | 5D07C93A286F2810009DE34B /* Collections */, 222 | ); 223 | productName = InteractiveGrid; 224 | productReference = 5D42D9EC270BDF340055D1E4 /* InteractiveGrid-UIKit.app */; 225 | productType = "com.apple.product-type.application"; 226 | }; 227 | 5D6FD1E5286F6F6C008F2487 /* Playground */ = { 228 | isa = PBXNativeTarget; 229 | buildConfigurationList = 5D6FD1EC286F6F6C008F2487 /* Build configuration list for PBXNativeTarget "Playground" */; 230 | buildPhases = ( 231 | 5D6FD1E2286F6F6C008F2487 /* Sources */, 232 | 5D6FD1E3286F6F6C008F2487 /* Frameworks */, 233 | 5D6FD1E4286F6F6C008F2487 /* CopyFiles */, 234 | ); 235 | buildRules = ( 236 | ); 237 | dependencies = ( 238 | ); 239 | name = Playground; 240 | packageProductDependencies = ( 241 | 5D6FD1EE286F6FBD008F2487 /* Collections */, 242 | 5D6FD1F1286F7001008F2487 /* Algorithms */, 243 | ); 244 | productName = Playground; 245 | productReference = 5D6FD1E6286F6F6C008F2487 /* Playground */; 246 | productType = "com.apple.product-type.tool"; 247 | }; 248 | /* End PBXNativeTarget section */ 249 | 250 | /* Begin PBXProject section */ 251 | 5D42D9E4270BDF340055D1E4 /* Project object */ = { 252 | isa = PBXProject; 253 | attributes = { 254 | BuildIndependentTargetsInParallel = 1; 255 | LastSwiftUpdateCheck = 1400; 256 | LastUpgradeCheck = 1400; 257 | TargetAttributes = { 258 | 5D07C926286F20B0009DE34B = { 259 | CreatedOnToolsVersion = 14.0; 260 | }; 261 | 5D42D9EB270BDF340055D1E4 = { 262 | CreatedOnToolsVersion = 13.0; 263 | LastSwiftMigration = 1300; 264 | }; 265 | 5D6FD1E5286F6F6C008F2487 = { 266 | CreatedOnToolsVersion = 14.0; 267 | }; 268 | }; 269 | }; 270 | buildConfigurationList = 5D42D9E7270BDF340055D1E4 /* Build configuration list for PBXProject "InteractiveGrid" */; 271 | compatibilityVersion = "Xcode 13.0"; 272 | developmentRegion = en; 273 | hasScannedForEncodings = 0; 274 | knownRegions = ( 275 | en, 276 | Base, 277 | ); 278 | mainGroup = 5D42D9E3270BDF340055D1E4; 279 | packageReferences = ( 280 | 5D07C939286F2810009DE34B /* XCRemoteSwiftPackageReference "swift-collections" */, 281 | 5D6FD1F0286F7001008F2487 /* XCRemoteSwiftPackageReference "swift-algorithms" */, 282 | ); 283 | productRefGroup = 5D42D9ED270BDF340055D1E4 /* Products */; 284 | projectDirPath = ""; 285 | projectRoot = ""; 286 | targets = ( 287 | 5D42D9EB270BDF340055D1E4 /* InteractiveGrid-UIKit */, 288 | 5D07C926286F20B0009DE34B /* InteractiveGrid-SwiftUI */, 289 | 5D6FD1E5286F6F6C008F2487 /* Playground */, 290 | ); 291 | }; 292 | /* End PBXProject section */ 293 | 294 | /* Begin PBXResourcesBuildPhase section */ 295 | 5D07C925286F20B0009DE34B /* Resources */ = { 296 | isa = PBXResourcesBuildPhase; 297 | buildActionMask = 2147483647; 298 | files = ( 299 | 5D07C932286F20B0009DE34B /* Preview Assets.xcassets in Resources */, 300 | 5D07C92E286F20B0009DE34B /* Assets.xcassets in Resources */, 301 | ); 302 | runOnlyForDeploymentPostprocessing = 0; 303 | }; 304 | 5D42D9EA270BDF340055D1E4 /* Resources */ = { 305 | isa = PBXResourcesBuildPhase; 306 | buildActionMask = 2147483647; 307 | files = ( 308 | 5DDC6446270DDFDD0059AA6D /* README.md in Resources */, 309 | 5D42D9F7270BDF350055D1E4 /* Preview Assets.xcassets in Resources */, 310 | 5D42D9F4270BDF350055D1E4 /* Assets.xcassets in Resources */, 311 | ); 312 | runOnlyForDeploymentPostprocessing = 0; 313 | }; 314 | /* End PBXResourcesBuildPhase section */ 315 | 316 | /* Begin PBXSourcesBuildPhase section */ 317 | 5D07C923286F20B0009DE34B /* Sources */ = { 318 | isa = PBXSourcesBuildPhase; 319 | buildActionMask = 2147483647; 320 | files = ( 321 | 5D07C937286F2476009DE34B /* Model.swift in Sources */, 322 | 5D07C92C286F20B0009DE34B /* ContentView.swift in Sources */, 323 | 5D07C92A286F20B0009DE34B /* InteractiveGridApp.swift in Sources */, 324 | ); 325 | runOnlyForDeploymentPostprocessing = 0; 326 | }; 327 | 5D42D9E8270BDF340055D1E4 /* Sources */ = { 328 | isa = PBXSourcesBuildPhase; 329 | buildActionMask = 2147483647; 330 | files = ( 331 | 5D42DA1E270BE0560055D1E4 /* InteractiveGridApp.swift in Sources */, 332 | 5D42DA1C270BE0440055D1E4 /* ContentView.swift in Sources */, 333 | 5D42DA17270BE00D0055D1E4 /* Model.swift in Sources */, 334 | 5D42DA14270BE00D0055D1E4 /* DynamicLayoutGroupProvider.swift in Sources */, 335 | 5D42DA16270BE00D0055D1E4 /* InteractiveGridViewController.swift in Sources */, 336 | 5D42DA15270BE00D0055D1E4 /* InteractiveGridViewControllerRepresentable.swift in Sources */, 337 | 5D42DA13270BE00D0055D1E4 /* DefaultDynamicLayoutGroupProvider.swift in Sources */, 338 | 5D921DDE2853FF59007C0EE4 /* GridCell.swift in Sources */, 339 | 5D42DA18270BE00D0055D1E4 /* GridCollectionViewCell.swift in Sources */, 340 | ); 341 | runOnlyForDeploymentPostprocessing = 0; 342 | }; 343 | 5D6FD1E2286F6F6C008F2487 /* Sources */ = { 344 | isa = PBXSourcesBuildPhase; 345 | buildActionMask = 2147483647; 346 | files = ( 347 | 5D6FD1E9286F6F6C008F2487 /* main.swift in Sources */, 348 | ); 349 | runOnlyForDeploymentPostprocessing = 0; 350 | }; 351 | /* End PBXSourcesBuildPhase section */ 352 | 353 | /* Begin XCBuildConfiguration section */ 354 | 5D07C934286F20B0009DE34B /* Debug */ = { 355 | isa = XCBuildConfiguration; 356 | buildSettings = { 357 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 358 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 359 | CODE_SIGN_ENTITLEMENTS = "InteractiveGrid-SwiftUI/InteractiveGrid_SwiftUI.entitlements"; 360 | CODE_SIGN_IDENTITY = "-"; 361 | CODE_SIGN_STYLE = Automatic; 362 | CURRENT_PROJECT_VERSION = 1; 363 | DEAD_CODE_STRIPPING = YES; 364 | DEVELOPMENT_ASSET_PATHS = "\"InteractiveGrid-SwiftUI/Preview Content\""; 365 | DEVELOPMENT_TEAM = KC5B683642; 366 | ENABLE_HARDENED_RUNTIME = YES; 367 | ENABLE_PREVIEWS = YES; 368 | GENERATE_INFOPLIST_FILE = YES; 369 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 370 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 371 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 372 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 373 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 374 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 375 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 376 | MACOSX_DEPLOYMENT_TARGET = 13.0; 377 | MARKETING_VERSION = 1.0; 378 | PRODUCT_BUNDLE_IDENTIFIER = "com.highrailcompany.InteractiveGrid-SwiftUI"; 379 | PRODUCT_NAME = "$(TARGET_NAME)"; 380 | SDKROOT = auto; 381 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 382 | SUPPORTS_MACCATALYST = NO; 383 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 384 | SWIFT_EMIT_LOC_STRINGS = YES; 385 | SWIFT_VERSION = 5.0; 386 | TARGETED_DEVICE_FAMILY = "1,2"; 387 | }; 388 | name = Debug; 389 | }; 390 | 5D07C935286F20B0009DE34B /* Release */ = { 391 | isa = XCBuildConfiguration; 392 | buildSettings = { 393 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 394 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 395 | CODE_SIGN_ENTITLEMENTS = "InteractiveGrid-SwiftUI/InteractiveGrid_SwiftUI.entitlements"; 396 | CODE_SIGN_IDENTITY = "-"; 397 | CODE_SIGN_STYLE = Automatic; 398 | CURRENT_PROJECT_VERSION = 1; 399 | DEAD_CODE_STRIPPING = YES; 400 | DEVELOPMENT_ASSET_PATHS = "\"InteractiveGrid-SwiftUI/Preview Content\""; 401 | DEVELOPMENT_TEAM = KC5B683642; 402 | ENABLE_HARDENED_RUNTIME = YES; 403 | ENABLE_PREVIEWS = YES; 404 | GENERATE_INFOPLIST_FILE = YES; 405 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 406 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 407 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 408 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 409 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 410 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 411 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 412 | MACOSX_DEPLOYMENT_TARGET = 13.0; 413 | MARKETING_VERSION = 1.0; 414 | PRODUCT_BUNDLE_IDENTIFIER = "com.highrailcompany.InteractiveGrid-SwiftUI"; 415 | PRODUCT_NAME = "$(TARGET_NAME)"; 416 | SDKROOT = auto; 417 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 418 | SUPPORTS_MACCATALYST = NO; 419 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 420 | SWIFT_EMIT_LOC_STRINGS = YES; 421 | SWIFT_VERSION = 5.0; 422 | TARGETED_DEVICE_FAMILY = "1,2"; 423 | }; 424 | name = Release; 425 | }; 426 | 5D42D9F8270BDF350055D1E4 /* Debug */ = { 427 | isa = XCBuildConfiguration; 428 | buildSettings = { 429 | ALWAYS_SEARCH_USER_PATHS = NO; 430 | CLANG_ANALYZER_NONNULL = YES; 431 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 432 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 433 | CLANG_CXX_LIBRARY = "libc++"; 434 | CLANG_ENABLE_MODULES = YES; 435 | CLANG_ENABLE_OBJC_ARC = YES; 436 | CLANG_ENABLE_OBJC_WEAK = YES; 437 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 438 | CLANG_WARN_BOOL_CONVERSION = YES; 439 | CLANG_WARN_COMMA = YES; 440 | CLANG_WARN_CONSTANT_CONVERSION = YES; 441 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 442 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 443 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 444 | CLANG_WARN_EMPTY_BODY = YES; 445 | CLANG_WARN_ENUM_CONVERSION = YES; 446 | CLANG_WARN_INFINITE_RECURSION = YES; 447 | CLANG_WARN_INT_CONVERSION = YES; 448 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 449 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 450 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 451 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 452 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 453 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 454 | CLANG_WARN_STRICT_PROTOTYPES = YES; 455 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 456 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 457 | CLANG_WARN_UNREACHABLE_CODE = YES; 458 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 459 | COPY_PHASE_STRIP = NO; 460 | DEBUG_INFORMATION_FORMAT = dwarf; 461 | ENABLE_STRICT_OBJC_MSGSEND = YES; 462 | ENABLE_TESTABILITY = YES; 463 | GCC_C_LANGUAGE_STANDARD = gnu11; 464 | GCC_DYNAMIC_NO_PIC = NO; 465 | GCC_NO_COMMON_BLOCKS = YES; 466 | GCC_OPTIMIZATION_LEVEL = 0; 467 | GCC_PREPROCESSOR_DEFINITIONS = ( 468 | "DEBUG=1", 469 | "$(inherited)", 470 | ); 471 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 472 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 473 | GCC_WARN_UNDECLARED_SELECTOR = YES; 474 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 475 | GCC_WARN_UNUSED_FUNCTION = YES; 476 | GCC_WARN_UNUSED_VARIABLE = YES; 477 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 478 | MTL_FAST_MATH = YES; 479 | ONLY_ACTIVE_ARCH = YES; 480 | SDKROOT = iphoneos; 481 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 482 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 483 | TARGETED_DEVICE_FAMILY = "1,2"; 484 | }; 485 | name = Debug; 486 | }; 487 | 5D42D9F9270BDF350055D1E4 /* Release */ = { 488 | isa = XCBuildConfiguration; 489 | buildSettings = { 490 | ALWAYS_SEARCH_USER_PATHS = NO; 491 | CLANG_ANALYZER_NONNULL = YES; 492 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 493 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 494 | CLANG_CXX_LIBRARY = "libc++"; 495 | CLANG_ENABLE_MODULES = YES; 496 | CLANG_ENABLE_OBJC_ARC = YES; 497 | CLANG_ENABLE_OBJC_WEAK = YES; 498 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 499 | CLANG_WARN_BOOL_CONVERSION = YES; 500 | CLANG_WARN_COMMA = YES; 501 | CLANG_WARN_CONSTANT_CONVERSION = YES; 502 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 503 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 504 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 505 | CLANG_WARN_EMPTY_BODY = YES; 506 | CLANG_WARN_ENUM_CONVERSION = YES; 507 | CLANG_WARN_INFINITE_RECURSION = YES; 508 | CLANG_WARN_INT_CONVERSION = YES; 509 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 510 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 511 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 512 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 513 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 514 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 515 | CLANG_WARN_STRICT_PROTOTYPES = YES; 516 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 517 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 518 | CLANG_WARN_UNREACHABLE_CODE = YES; 519 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 520 | COPY_PHASE_STRIP = NO; 521 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 522 | ENABLE_NS_ASSERTIONS = NO; 523 | ENABLE_STRICT_OBJC_MSGSEND = YES; 524 | GCC_C_LANGUAGE_STANDARD = gnu11; 525 | GCC_NO_COMMON_BLOCKS = YES; 526 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 527 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 528 | GCC_WARN_UNDECLARED_SELECTOR = YES; 529 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 530 | GCC_WARN_UNUSED_FUNCTION = YES; 531 | GCC_WARN_UNUSED_VARIABLE = YES; 532 | MTL_ENABLE_DEBUG_INFO = NO; 533 | MTL_FAST_MATH = YES; 534 | SDKROOT = iphoneos; 535 | SWIFT_COMPILATION_MODE = wholemodule; 536 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 537 | TARGETED_DEVICE_FAMILY = "1,2"; 538 | VALIDATE_PRODUCT = YES; 539 | }; 540 | name = Release; 541 | }; 542 | 5D42D9FB270BDF350055D1E4 /* Debug */ = { 543 | isa = XCBuildConfiguration; 544 | buildSettings = { 545 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 546 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 547 | CLANG_ENABLE_MODULES = YES; 548 | CODE_SIGN_STYLE = Automatic; 549 | CURRENT_PROJECT_VERSION = 1; 550 | DEVELOPMENT_ASSET_PATHS = "\"InteractiveGrid-UIKit/Preview Content\""; 551 | DEVELOPMENT_TEAM = KC5B683642; 552 | ENABLE_PREVIEWS = YES; 553 | GENERATE_INFOPLIST_FILE = YES; 554 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 555 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 556 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 557 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 558 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 559 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 560 | LD_RUNPATH_SEARCH_PATHS = ( 561 | "$(inherited)", 562 | "@executable_path/Frameworks", 563 | ); 564 | MARKETING_VERSION = 1.0; 565 | PRODUCT_BUNDLE_IDENTIFIER = com.highrailcompany.InteractiveGrid; 566 | PRODUCT_NAME = "$(TARGET_NAME)"; 567 | SWIFT_EMIT_LOC_STRINGS = YES; 568 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 569 | SWIFT_VERSION = 5.0; 570 | TARGETED_DEVICE_FAMILY = "1,2"; 571 | }; 572 | name = Debug; 573 | }; 574 | 5D42D9FC270BDF350055D1E4 /* Release */ = { 575 | isa = XCBuildConfiguration; 576 | buildSettings = { 577 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 578 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 579 | CLANG_ENABLE_MODULES = YES; 580 | CODE_SIGN_STYLE = Automatic; 581 | CURRENT_PROJECT_VERSION = 1; 582 | DEVELOPMENT_ASSET_PATHS = "\"InteractiveGrid-UIKit/Preview Content\""; 583 | DEVELOPMENT_TEAM = KC5B683642; 584 | ENABLE_PREVIEWS = YES; 585 | GENERATE_INFOPLIST_FILE = YES; 586 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 587 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 588 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 589 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 590 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 591 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 592 | LD_RUNPATH_SEARCH_PATHS = ( 593 | "$(inherited)", 594 | "@executable_path/Frameworks", 595 | ); 596 | MARKETING_VERSION = 1.0; 597 | PRODUCT_BUNDLE_IDENTIFIER = com.highrailcompany.InteractiveGrid; 598 | PRODUCT_NAME = "$(TARGET_NAME)"; 599 | SWIFT_EMIT_LOC_STRINGS = YES; 600 | SWIFT_VERSION = 5.0; 601 | TARGETED_DEVICE_FAMILY = "1,2"; 602 | }; 603 | name = Release; 604 | }; 605 | 5D6FD1EA286F6F6C008F2487 /* Debug */ = { 606 | isa = XCBuildConfiguration; 607 | buildSettings = { 608 | CODE_SIGN_IDENTITY = "-"; 609 | CODE_SIGN_STYLE = Automatic; 610 | DEAD_CODE_STRIPPING = YES; 611 | DEVELOPMENT_TEAM = KC5B683642; 612 | ENABLE_HARDENED_RUNTIME = YES; 613 | MACOSX_DEPLOYMENT_TARGET = 12.4; 614 | PRODUCT_NAME = "$(TARGET_NAME)"; 615 | SDKROOT = macosx; 616 | SWIFT_VERSION = 5.0; 617 | }; 618 | name = Debug; 619 | }; 620 | 5D6FD1EB286F6F6C008F2487 /* Release */ = { 621 | isa = XCBuildConfiguration; 622 | buildSettings = { 623 | CODE_SIGN_IDENTITY = "-"; 624 | CODE_SIGN_STYLE = Automatic; 625 | DEAD_CODE_STRIPPING = YES; 626 | DEVELOPMENT_TEAM = KC5B683642; 627 | ENABLE_HARDENED_RUNTIME = YES; 628 | MACOSX_DEPLOYMENT_TARGET = 12.4; 629 | PRODUCT_NAME = "$(TARGET_NAME)"; 630 | SDKROOT = macosx; 631 | SWIFT_VERSION = 5.0; 632 | }; 633 | name = Release; 634 | }; 635 | /* End XCBuildConfiguration section */ 636 | 637 | /* Begin XCConfigurationList section */ 638 | 5D07C933286F20B0009DE34B /* Build configuration list for PBXNativeTarget "InteractiveGrid-SwiftUI" */ = { 639 | isa = XCConfigurationList; 640 | buildConfigurations = ( 641 | 5D07C934286F20B0009DE34B /* Debug */, 642 | 5D07C935286F20B0009DE34B /* Release */, 643 | ); 644 | defaultConfigurationIsVisible = 0; 645 | defaultConfigurationName = Release; 646 | }; 647 | 5D42D9E7270BDF340055D1E4 /* Build configuration list for PBXProject "InteractiveGrid" */ = { 648 | isa = XCConfigurationList; 649 | buildConfigurations = ( 650 | 5D42D9F8270BDF350055D1E4 /* Debug */, 651 | 5D42D9F9270BDF350055D1E4 /* Release */, 652 | ); 653 | defaultConfigurationIsVisible = 0; 654 | defaultConfigurationName = Release; 655 | }; 656 | 5D42D9FA270BDF350055D1E4 /* Build configuration list for PBXNativeTarget "InteractiveGrid-UIKit" */ = { 657 | isa = XCConfigurationList; 658 | buildConfigurations = ( 659 | 5D42D9FB270BDF350055D1E4 /* Debug */, 660 | 5D42D9FC270BDF350055D1E4 /* Release */, 661 | ); 662 | defaultConfigurationIsVisible = 0; 663 | defaultConfigurationName = Release; 664 | }; 665 | 5D6FD1EC286F6F6C008F2487 /* Build configuration list for PBXNativeTarget "Playground" */ = { 666 | isa = XCConfigurationList; 667 | buildConfigurations = ( 668 | 5D6FD1EA286F6F6C008F2487 /* Debug */, 669 | 5D6FD1EB286F6F6C008F2487 /* Release */, 670 | ); 671 | defaultConfigurationIsVisible = 0; 672 | defaultConfigurationName = Release; 673 | }; 674 | /* End XCConfigurationList section */ 675 | 676 | /* Begin XCRemoteSwiftPackageReference section */ 677 | 5D07C939286F2810009DE34B /* XCRemoteSwiftPackageReference "swift-collections" */ = { 678 | isa = XCRemoteSwiftPackageReference; 679 | repositoryURL = "https://github.com/apple/swift-collections.git"; 680 | requirement = { 681 | kind = upToNextMajorVersion; 682 | minimumVersion = 1.0.0; 683 | }; 684 | }; 685 | 5D6FD1F0286F7001008F2487 /* XCRemoteSwiftPackageReference "swift-algorithms" */ = { 686 | isa = XCRemoteSwiftPackageReference; 687 | repositoryURL = "https://github.com/apple/swift-algorithms.git"; 688 | requirement = { 689 | kind = upToNextMajorVersion; 690 | minimumVersion = 1.0.0; 691 | }; 692 | }; 693 | /* End XCRemoteSwiftPackageReference section */ 694 | 695 | /* Begin XCSwiftPackageProductDependency section */ 696 | 5D07C93A286F2810009DE34B /* Collections */ = { 697 | isa = XCSwiftPackageProductDependency; 698 | package = 5D07C939286F2810009DE34B /* XCRemoteSwiftPackageReference "swift-collections" */; 699 | productName = Collections; 700 | }; 701 | 5D6FD1EE286F6FBD008F2487 /* Collections */ = { 702 | isa = XCSwiftPackageProductDependency; 703 | package = 5D07C939286F2810009DE34B /* XCRemoteSwiftPackageReference "swift-collections" */; 704 | productName = Collections; 705 | }; 706 | 5D6FD1F1286F7001008F2487 /* Algorithms */ = { 707 | isa = XCSwiftPackageProductDependency; 708 | package = 5D6FD1F0286F7001008F2487 /* XCRemoteSwiftPackageReference "swift-algorithms" */; 709 | productName = Algorithms; 710 | }; 711 | 5DAFABDB286F91BA00D72C07 /* Algorithms */ = { 712 | isa = XCSwiftPackageProductDependency; 713 | package = 5D6FD1F0286F7001008F2487 /* XCRemoteSwiftPackageReference "swift-algorithms" */; 714 | productName = Algorithms; 715 | }; 716 | /* End XCSwiftPackageProductDependency section */ 717 | }; 718 | rootObject = 5D42D9E4270BDF340055D1E4 /* Project object */; 719 | } 720 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-algorithms", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-algorithms.git", 7 | "state" : { 8 | "revision" : "b14b7f4c528c942f121c8b860b9410b2bf57825e", 9 | "version" : "1.0.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-collections", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-collections.git", 16 | "state" : { 17 | "revision" : "48254824bb4248676bf7ce56014ff57b142b77eb", 18 | "version" : "1.0.2" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-numerics", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-numerics", 25 | "state" : { 26 | "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", 27 | "version" : "1.0.2" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /InteractiveGrid/InteractiveGrid.xcodeproj/xcshareddata/xcschemes/InteractiveGrid.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /InteractiveGrid/Playground/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // Playground 4 | // 5 | // Created by Brian Coyner on 7/1/22. 6 | // 7 | 8 | import Foundation 9 | import Algorithms 10 | 11 | 12 | 13 | struct Model: Hashable, Identifiable { 14 | enum Style { 15 | case compact 16 | case regular 17 | } 18 | 19 | var id: Int { 20 | return value 21 | } 22 | 23 | let value: Int 24 | let style: Style 25 | let allowsContextMenu: Bool 26 | } 27 | 28 | extension Model.Style: CustomStringConvertible { 29 | 30 | var description: String { 31 | switch self { 32 | case .compact: 33 | return "Compact" 34 | case .regular: 35 | return "Regular" 36 | } 37 | } 38 | 39 | var symbolName: String { 40 | switch self { 41 | case .compact: 42 | return "plus.square" 43 | case .regular: 44 | return "plus.rectangle" 45 | } 46 | } 47 | } 48 | 49 | extension Model { 50 | 51 | static func makeAllCompact() -> [Model] { 52 | return [ 53 | Model(value: 1, style: .compact, allowsContextMenu: true), 54 | Model(value: 2, style: .compact, allowsContextMenu: true), 55 | Model(value: 3, style: .compact, allowsContextMenu: true), 56 | Model(value: 4, style: .compact, allowsContextMenu: true), 57 | Model(value: 5, style: .compact, allowsContextMenu: true), 58 | Model(value: 6, style: .compact, allowsContextMenu: true) 59 | ] 60 | } 61 | 62 | static func makeAllRegular() -> [Model] { 63 | return [ 64 | Model(value: 1, style: .regular, allowsContextMenu: false), 65 | Model(value: 2, style: .regular, allowsContextMenu: false), 66 | Model(value: 3, style: .regular, allowsContextMenu: false), 67 | Model(value: 4, style: .regular, allowsContextMenu: false), 68 | Model(value: 5, style: .regular, allowsContextMenu: false), 69 | Model(value: 6, style: .regular, allowsContextMenu: false) 70 | ] 71 | } 72 | 73 | static func makeMixNMatch() -> [Model] { 74 | return [ 75 | Model(value: 1, style: .regular, allowsContextMenu: true), 76 | Model(value: 2, style: .compact, allowsContextMenu: true), 77 | Model(value: 3, style: .compact, allowsContextMenu: true), 78 | Model(value: 4, style: .compact, allowsContextMenu: true), 79 | Model(value: 5, style: .compact, allowsContextMenu: true), 80 | Model(value: 6, style: .regular, allowsContextMenu: true), 81 | Model(value: 7, style: .compact, allowsContextMenu: true), 82 | Model(value: 8, style: .compact, allowsContextMenu: true), 83 | Model(value: 9, style: .regular, allowsContextMenu: true) 84 | ] 85 | } 86 | 87 | static func makeRandom() -> [Model] { 88 | let compactModels = (0..