From 50c14fad6eac95577aa98e6a6f75efb440346834 Mon Sep 17 00:00:00 2001 From: Alex Young Date: Sat, 28 Mar 2020 22:41:33 -0700 Subject: [PATCH] More examples (#91) * sectioned grid ios * layout and examples --- GridDemo iOS/ContentView.swift | 14 +++++ .../SectionedGrid/SectionedGridView.swift | 58 +++++++++++++++++ GridDemo iOS/StaticGrid/StaticGridView.swift | 33 ++++++++++ GridDemo macOS/ContentView.swift | 4 ++ ...onedGrid.swift => SectionedGridView.swift} | 0 .../StaticGrid/StaticGridView.swift | 32 ++++++++++ GridDemo.xcodeproj/project.pbxproj | 48 ++++++++++++-- LICENSE | 2 +- README.md | 1 - Sources/Grid/Grid.swift | 16 ++--- Sources/Grid/Styles/ModularGridStyle.swift | 18 +++--- Sources/Grid/Styles/StaggeredGridStyle.swift | 18 +++--- .../Grid/Styles/Style/GridPreferences.swift | 19 ++---- .../Styles/Style/GridPreferencesKey.swift | 10 +++ Tests/GridTests/GridPreferencesTests.swift | 30 +++++++++ Tests/GridTests/ModularGridStyleTests.swift | 62 +++++++++++++++++++ Tests/GridTests/StaggeredGridStyleTests.swift | 27 ++++++++ 17 files changed, 345 insertions(+), 47 deletions(-) create mode 100644 GridDemo iOS/SectionedGrid/SectionedGridView.swift create mode 100644 GridDemo iOS/StaticGrid/StaticGridView.swift rename GridDemo macOS/SectionedGrid/{SectionedGrid.swift => SectionedGridView.swift} (100%) create mode 100644 GridDemo macOS/StaticGrid/StaticGridView.swift create mode 100644 Sources/Grid/Styles/Style/GridPreferencesKey.swift create mode 100644 Tests/GridTests/GridPreferencesTests.swift create mode 100644 Tests/GridTests/ModularGridStyleTests.swift create mode 100644 Tests/GridTests/StaggeredGridStyleTests.swift diff --git a/GridDemo iOS/ContentView.swift b/GridDemo iOS/ContentView.swift index bd6c0d9..9d8c88b 100644 --- a/GridDemo iOS/ContentView.swift +++ b/GridDemo iOS/ContentView.swift @@ -19,6 +19,20 @@ struct ContentView: View { Text("Staggered Grid") } } + + NavigationLink(destination: SectionedGridView()) { + HStack { + Image(systemName: "rectangle.grid.1x2.fill") + Text("Sectioned Grid") + } + } + + NavigationLink(destination: StaticGridView()) { + HStack { + Image(systemName: "circle.grid.2x2.fill") + Text("Static Grid") + } + } } } .listStyle( diff --git a/GridDemo iOS/SectionedGrid/SectionedGridView.swift b/GridDemo iOS/SectionedGrid/SectionedGridView.swift new file mode 100644 index 0000000..8bd21f0 --- /dev/null +++ b/GridDemo iOS/SectionedGrid/SectionedGridView.swift @@ -0,0 +1,58 @@ +import SwiftUI +import Grid + +struct SectionedGridView: View { + var body: some View { + ScrollView { + ForEach(1..<8) { section in + Section { + HStack { + Text("Section \(section)").font(.headline).fontWeight(.bold) + Spacer() + } + + Grid(self.rangeFor(section: section), id: \.self) { index in + Rectangle() + .foregroundColor(.clear) + .background( + Image("\(index)") + .renderingMode(.original) + .resizable() + .scaledToFill() + ) + .clipped() + .clipShape(RoundedRectangle(cornerRadius: 4)) + } + + Spacer(minLength: 16) + } + } + + .padding(8) + } + .navigationBarTitle("Sectioned Grid", displayMode: .inline) + .gridStyle( + ModularGridStyle(columns: .min(80), rows: .fixed(80), spacing: 4) + ) + } + + private func rangeFor(section: Int) -> Range { + switch section { + case 1: return Range(1...10) + case 2: return Range(11...15) + case 3: return Range(16...26) + case 4: return Range(27...35) + case 5: return Range(36...38) + case 6: return Range(39...55) + case 7: return Range(56...69) + default: + fatalError() + } + } +} + +struct SectionedGridView_Previews: PreviewProvider { + static var previews: some View { + SectionedGridView() + } +} diff --git a/GridDemo iOS/StaticGrid/StaticGridView.swift b/GridDemo iOS/StaticGrid/StaticGridView.swift new file mode 100644 index 0000000..46162a6 --- /dev/null +++ b/GridDemo iOS/StaticGrid/StaticGridView.swift @@ -0,0 +1,33 @@ +import SwiftUI +import Grid + +struct StaticGridView: View { + var body: some View { + ScrollView { + Grid { + Capsule().foregroundColor(.random) + Capsule().foregroundColor(.random) + Capsule().foregroundColor(.random) + Capsule().foregroundColor(.random) + Capsule().foregroundColor(.random) + Capsule().foregroundColor(.random) + Capsule().foregroundColor(.random) + Capsule().foregroundColor(.random) + Capsule().foregroundColor(.random) + Capsule().foregroundColor(.random) + } + .padding(16) + .frame(height: 300) + } + .navigationBarTitle("Static Grid", displayMode: .inline) + .gridStyle( + ModularGridStyle(columns: 5, rows: 2, spacing: 16) + ) + } +} + +struct StaticGridView_Previews: PreviewProvider { + static var previews: some View { + StaticGridView() + } +} diff --git a/GridDemo macOS/ContentView.swift b/GridDemo macOS/ContentView.swift index c8f41e4..c8d467b 100644 --- a/GridDemo macOS/ContentView.swift +++ b/GridDemo macOS/ContentView.swift @@ -16,6 +16,10 @@ struct ContentView: View { NavigationLink(destination: SectionedGridView()) { Text("Sectioned Grid") } + + NavigationLink(destination: StaticGridView()) { + Text("Static Grid") + } } } .frame(minWidth: 200, maxWidth: 300) diff --git a/GridDemo macOS/SectionedGrid/SectionedGrid.swift b/GridDemo macOS/SectionedGrid/SectionedGridView.swift similarity index 100% rename from GridDemo macOS/SectionedGrid/SectionedGrid.swift rename to GridDemo macOS/SectionedGrid/SectionedGridView.swift diff --git a/GridDemo macOS/StaticGrid/StaticGridView.swift b/GridDemo macOS/StaticGrid/StaticGridView.swift new file mode 100644 index 0000000..2d53049 --- /dev/null +++ b/GridDemo macOS/StaticGrid/StaticGridView.swift @@ -0,0 +1,32 @@ +import SwiftUI +import Grid + +struct StaticGridView: View { + var body: some View { + ScrollView { + Grid { + Capsule().foregroundColor(.random) + Capsule().foregroundColor(.random) + Capsule().foregroundColor(.random) + Capsule().foregroundColor(.random) + Capsule().foregroundColor(.random) + Capsule().foregroundColor(.random) + Capsule().foregroundColor(.random) + Capsule().foregroundColor(.random) + Capsule().foregroundColor(.random) + Capsule().foregroundColor(.random) + } + .padding(16) + .frame(height: 300) + } + .gridStyle( + ModularGridStyle(columns: 5, rows: 2, spacing: 16) + ) + } +} + +struct StaticGridView_Previews: PreviewProvider { + static var previews: some View { + StaticGridView() + } +} diff --git a/GridDemo.xcodeproj/project.pbxproj b/GridDemo.xcodeproj/project.pbxproj index 34188b6..8d0c3c7 100644 --- a/GridDemo.xcodeproj/project.pbxproj +++ b/GridDemo.xcodeproj/project.pbxproj @@ -8,7 +8,9 @@ /* Begin PBXBuildFile section */ FA6203C323F8AD77009CB0C9 /* Grid in Frameworks */ = {isa = PBXBuildFile; productRef = FA6203C223F8AD77009CB0C9 /* Grid */; }; - FA9FFB6D242FFECD0047D145 /* SectionedGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9FFB6C242FFECD0047D145 /* SectionedGrid.swift */; }; + FA9FFB6D242FFECD0047D145 /* SectionedGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9FFB6C242FFECD0047D145 /* SectionedGridView.swift */; }; + FA9FFB70243005B80047D145 /* SectionedGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9FFB6F243005B80047D145 /* SectionedGridView.swift */; }; + FA9FFB74243008060047D145 /* StaticGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9FFB73243008060047D145 /* StaticGridView.swift */; }; FAA2C96C23DEAECC00FBDE39 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA2C96B23DEAECC00FBDE39 /* SceneDelegate.swift */; }; FAA2C96E23DEAECC00FBDE39 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA2C96D23DEAECC00FBDE39 /* ContentView.swift */; }; FAA2C97623DEAECD00FBDE39 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FAA2C97423DEAECD00FBDE39 /* LaunchScreen.storyboard */; }; @@ -66,6 +68,7 @@ FAA2CA5523DEB66A00FBDE39 /* StaggeredGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA2CA5323DEB66900FBDE39 /* StaggeredGridView.swift */; }; FAA2CA5923DEB73200FBDE39 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = FAA2CA5823DEB73200FBDE39 /* README.md */; }; FAAA2FCD241C42E4007021F7 /* ImageDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAAA2FCC241C42E4007021F7 /* ImageDetailView.swift */; }; + FAC1C78B2430694C00FFA885 /* StaticGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC1C78A2430694C00FFA885 /* StaticGridView.swift */; }; FAC9D4DE23DF8CE20007B7DD /* Grid in Frameworks */ = {isa = PBXBuildFile; productRef = FAC9D4DD23DF8CE20007B7DD /* Grid */; }; FAC9D4E023DF8CE90007B7DD /* Grid in Frameworks */ = {isa = PBXBuildFile; productRef = FAC9D4DF23DF8CE90007B7DD /* Grid */; }; FAC9D4E223DF8CEE0007B7DD /* Grid in Frameworks */ = {isa = PBXBuildFile; productRef = FAC9D4E123DF8CEE0007B7DD /* Grid */; }; @@ -114,7 +117,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - FA9FFB6C242FFECD0047D145 /* SectionedGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionedGrid.swift; sourceTree = ""; }; + FA9FFB6C242FFECD0047D145 /* SectionedGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionedGridView.swift; sourceTree = ""; }; + FA9FFB6F243005B80047D145 /* SectionedGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionedGridView.swift; sourceTree = ""; }; + FA9FFB73243008060047D145 /* StaticGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticGridView.swift; sourceTree = ""; }; FAA2C96723DEAECC00FBDE39 /* GridDemo iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "GridDemo iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; FAA2C96B23DEAECC00FBDE39 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; FAA2C96D23DEAECC00FBDE39 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -171,7 +176,8 @@ FAA2CA5323DEB66900FBDE39 /* StaggeredGridView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StaggeredGridView.swift; sourceTree = ""; }; FAA2CA5823DEB73200FBDE39 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; FAAA2FCC241C42E4007021F7 /* ImageDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDetailView.swift; sourceTree = ""; }; - FAC9D4DC23DF8C750007B7DD /* */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ""; sourceTree = SOURCE_ROOT; }; + FAC1C7882430682100FFA885 /* swiftui-grid */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "swiftui-grid"; sourceTree = ""; }; + FAC1C78A2430694C00FFA885 /* StaticGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticGridView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -213,11 +219,27 @@ FA9FFB6B242FFE510047D145 /* SectionedGrid */ = { isa = PBXGroup; children = ( - FA9FFB6C242FFECD0047D145 /* SectionedGrid.swift */, + FA9FFB6C242FFECD0047D145 /* SectionedGridView.swift */, ); path = SectionedGrid; sourceTree = ""; }; + FA9FFB6E2430057F0047D145 /* SectionedGrid */ = { + isa = PBXGroup; + children = ( + FA9FFB6F243005B80047D145 /* SectionedGridView.swift */, + ); + path = SectionedGrid; + sourceTree = ""; + }; + FA9FFB72243007EA0047D145 /* StaticGrid */ = { + isa = PBXGroup; + children = ( + FA9FFB73243008060047D145 /* StaticGridView.swift */, + ); + path = StaticGrid; + sourceTree = ""; + }; FAA2C93B23DEAD2600FBDE39 = { isa = PBXGroup; children = ( @@ -230,7 +252,7 @@ FAA2CA3B23DEB64800FBDE39 /* GridDemo tvOS */, FAA2C94923DEAE7B00FBDE39 /* Products */, FAA2C99423DEB0A700FBDE39 /* Frameworks */, - FAC9D4DC23DF8C750007B7DD /* */, + FAC1C7882430682100FFA885 /* swiftui-grid */, ); sourceTree = ""; }; @@ -250,6 +272,8 @@ FAA2C96823DEAECC00FBDE39 /* GridDemo iOS */ = { isa = PBXGroup; children = ( + FAC1C7892430693900FFA885 /* StaticGrid */, + FA9FFB6E2430057F0047D145 /* SectionedGrid */, FAA2C98823DEB00100FBDE39 /* ModularGrid */, FAA2C98B23DEB00100FBDE39 /* StaggeredGrid */, FAA2C97B23DEAF9200FBDE39 /* AppDelegate.swift */, @@ -311,6 +335,7 @@ FAA2C99D23DEB26200FBDE39 /* GridDemo macOS */ = { isa = PBXGroup; children = ( + FA9FFB72243007EA0047D145 /* StaticGrid */, FA9FFB6B242FFE510047D145 /* SectionedGrid */, FAA2C9B323DEB31800FBDE39 /* ModularGrid */, FAA2C9B623DEB31800FBDE39 /* StaggeredGrid */, @@ -442,6 +467,14 @@ path = StaggeredGrid; sourceTree = ""; }; + FAC1C7892430693900FFA885 /* StaticGrid */ = { + isa = PBXGroup; + children = ( + FAC1C78A2430694C00FFA885 /* StaticGridView.swift */, + ); + path = StaticGrid; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -683,6 +716,7 @@ buildActionMask = 2147483647; files = ( FAA2C98523DEAFF100FBDE39 /* Item.swift in Sources */, + FA9FFB70243005B80047D145 /* SectionedGridView.swift in Sources */, FAA2C99123DEB00100FBDE39 /* StaggeredGridView.swift in Sources */, FAA2C99023DEB00100FBDE39 /* StaggeredGridSettingsView.swift in Sources */, FAAA2FCD241C42E4007021F7 /* ImageDetailView.swift in Sources */, @@ -693,6 +727,7 @@ FAA2C96E23DEAECC00FBDE39 /* ContentView.swift in Sources */, FAA2C98F23DEB00100FBDE39 /* ModularGridSettingsView.swift in Sources */, FAA2C98E23DEB00100FBDE39 /* ModularGridView.swift in Sources */, + FAC1C78B2430694C00FFA885 /* StaticGridView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -700,7 +735,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - FA9FFB6D242FFECD0047D145 /* SectionedGrid.swift in Sources */, + FA9FFB6D242FFECD0047D145 /* SectionedGridView.swift in Sources */, + FA9FFB74243008060047D145 /* StaticGridView.swift in Sources */, FAA2C9B023DEB26B00FBDE39 /* Item.swift in Sources */, FAA2C9AF23DEB26B00FBDE39 /* Color+Random.swift in Sources */, FAA2C9BB23DEB31900FBDE39 /* StaggeredGridSettingsView.swift in Sources */, diff --git a/LICENSE b/LICENSE index 9fca77d..3f63563 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 SwiftExtensions. +Copyright (c) 2020 SpaceNation Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index d63580b..0f6ba30 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ SwiftUI Grid view layout with custom styles. ## Features - ZStack based layout - Vertical and horizontal scrolling -- Supports grid of grids, each with it's own style - Supports all apple platforms - Custom styles (ModularGridStyle, StaggeredGridStyle) - SwiftUI code patterns (StyleStructs, EnvironmentValues, ViewBuilder) diff --git a/Sources/Grid/Grid.swift b/Sources/Grid/Grid.swift index 261f7c5..0aa5172 100644 --- a/Sources/Grid/Grid.swift +++ b/Sources/Grid/Grid.swift @@ -3,7 +3,7 @@ import SwiftUI /// A view that arranges its children in a grid. public struct Grid: View where Content: View { @Environment(\.gridStyle) private var style - @State var preferences: GridPreferences = .init(items: []) + @State var preferences: GridPreferences = GridPreferences(size: .zero, items: []) let items: [GridItem] public var body: some View { @@ -15,8 +15,8 @@ public struct Grid: View where Content: View { width: self.style.autoWidth ? self.preferences[item.id]?.bounds.width : nil, height: self.style.autoHeight ? self.preferences[item.id]?.bounds.height : nil ) - .alignmentGuide(.leading, computeValue: { _ in self.preferences[item.id]?.bounds.origin.x ?? 0 }) - .alignmentGuide(.top, computeValue: { _ in self.preferences[item.id]?.bounds.origin.y ?? 0 }) + .alignmentGuide(.leading, computeValue: { _ in geometry.size.width - (self.preferences[item.id]?.bounds.origin.x ?? 0) }) + .alignmentGuide(.top, computeValue: { _ in geometry.size.height - (self.preferences[item.id]?.bounds.origin.y ?? 0) }) .background(GridPreferencesModifier(id: item.id, bounds: self.preferences[item.id]?.bounds ?? .zero)) .anchorPreference(key: GridItemBoundsPreferencesKey.self, value: .bounds) { [geometry[$0]] } } @@ -25,14 +25,14 @@ public struct Grid: View where Content: View { self.style.transform(preferences: &$0, in: geometry.size) } } - .onPreferenceChange(GridPreferencesKey.self) { preferences in - self.preferences = preferences - } .frame( - width: self.style.axis == .horizontal ? self.preferences.size.width : nil, - height: self.style.axis == .vertical ? self.preferences.size.height : nil, + minWidth: self.style.axis == .horizontal ? self.preferences.size.width : nil, + minHeight: self.style.axis == .vertical ? self.preferences.size.height : nil, alignment: .topLeading ) + .onPreferenceChange(GridPreferencesKey.self) { preferences in + self.preferences = preferences + } } } diff --git a/Sources/Grid/Styles/ModularGridStyle.swift b/Sources/Grid/Styles/ModularGridStyle.swift index 99f451e..09564e6 100644 --- a/Sources/Grid/Styles/ModularGridStyle.swift +++ b/Sources/Grid/Styles/ModularGridStyle.swift @@ -42,34 +42,34 @@ public struct ModularGridStyle: GridStyle { ) ) - preferences.items = layoutPreferences( + preferences = layoutPreferences( tracks: computedTracksCount, spacing: self.spacing, axis: self.axis, itemSize: itemSize, - preferences: preferences.items + preferences: preferences ) } - private func layoutPreferences(tracks: Int, spacing: CGFloat, axis: Axis, itemSize: CGSize, preferences: [GridPreferences.Item]) -> [GridPreferences.Item] { + private func layoutPreferences(tracks: Int, spacing: CGFloat, axis: Axis, itemSize: CGSize, preferences: GridPreferences) -> GridPreferences { var tracksLengths = Array(repeating: CGFloat(0.0), count: tracks) - var newPreferences: [GridPreferences.Item] = [] + var newPreferences: GridPreferences = GridPreferences(items: []) - preferences.forEach { preference in + preferences.items.forEach { preference in if let minValue = tracksLengths.min(), let indexMin = tracksLengths.firstIndex(of: minValue) { let itemSizeWidth = itemSize.width let itemSizeHeight = itemSize.height let width = axis == .vertical ? itemSizeWidth * CGFloat(indexMin) + CGFloat(indexMin) * spacing : tracksLengths[indexMin] let height = axis == .vertical ? tracksLengths[indexMin] : itemSizeHeight * CGFloat(indexMin) + CGFloat(indexMin) * spacing - let origin = CGPoint(x: 0 - width, y: 0 - height) + let origin = CGPoint(x: width, y: height) tracksLengths[indexMin] += (axis == .vertical ? itemSizeHeight : itemSizeWidth) + spacing - newPreferences.append( - GridPreferences.Item( + newPreferences.merge(with: + GridPreferences(items: [GridPreferences.Item( id: preference.id, bounds: CGRect(origin: origin, size: CGSize(width: itemSizeWidth, height: itemSizeHeight)) - ) + )]) ) } } diff --git a/Sources/Grid/Styles/StaggeredGridStyle.swift b/Sources/Grid/Styles/StaggeredGridStyle.swift index 0ebdd10..072b3b9 100644 --- a/Sources/Grid/Styles/StaggeredGridStyle.swift +++ b/Sources/Grid/Styles/StaggeredGridStyle.swift @@ -45,34 +45,34 @@ public struct StaggeredGridStyle: GridStyle { ) ) - preferences.items = layoutPreferences( + preferences = layoutPreferences( tracks: computedTracksCount, spacing: self.spacing, axis: self.axis, itemSize: size, - preferences: preferences.items + preferences: preferences ) } - private func layoutPreferences(tracks: Int, spacing: CGFloat, axis: Axis, itemSize: CGSize, preferences: [GridPreferences.Item]) -> [GridPreferences.Item] { + private func layoutPreferences(tracks: Int, spacing: CGFloat, axis: Axis, itemSize: CGSize, preferences: GridPreferences) -> GridPreferences { var tracksLengths = Array(repeating: CGFloat(0.0), count: tracks) - var newPreferences: [GridPreferences.Item] = [] + var newPreferences: GridPreferences = GridPreferences(items: []) - preferences.forEach { preference in + preferences.items.forEach { preference in if let minValue = tracksLengths.min(), let indexMin = tracksLengths.firstIndex(of: minValue) { let itemSizeWidth = axis == .vertical ? itemSize.width : preference.bounds.size.width let itemSizeHeight = axis == .vertical ? preference.bounds.size.height : itemSize.height let width = axis == .vertical ? itemSizeWidth * CGFloat(indexMin) + CGFloat(indexMin) * spacing : tracksLengths[indexMin] let height = axis == .vertical ? tracksLengths[indexMin] : itemSizeHeight * CGFloat(indexMin) + CGFloat(indexMin) * spacing - let origin = CGPoint(x: 0 - width, y: 0 - height) + let origin = CGPoint(x: width, y: height) tracksLengths[indexMin] += (axis == .vertical ? itemSizeHeight : itemSizeWidth) + spacing - newPreferences.append( - GridPreferences.Item( + newPreferences.merge(with: + GridPreferences(items: [GridPreferences.Item( id: preference.id, bounds: CGRect(origin: origin, size: CGSize(width: itemSizeWidth, height: itemSizeHeight)) - ) + )]) ) } } diff --git a/Sources/Grid/Styles/Style/GridPreferences.swift b/Sources/Grid/Styles/Style/GridPreferences.swift index 9c143de..e42cc5e 100644 --- a/Sources/Grid/Styles/Style/GridPreferences.swift +++ b/Sources/Grid/Styles/Style/GridPreferences.swift @@ -1,5 +1,5 @@ import Foundation -import SwiftUI +import CoreGraphics public struct GridPreferences: Equatable { public struct Item: Equatable { @@ -9,9 +9,10 @@ public struct GridPreferences: Equatable { public var items: [Item] - public var size: CGSize = .zero + public var size: CGSize - public init(items: [Item]) { + public init(size: CGSize = .zero, items: [Item]) { + self.size = size self.items = items } @@ -24,16 +25,8 @@ public struct GridPreferences: Equatable { mutating func merge(with preferences: GridPreferences) { self.items.append(contentsOf: preferences.items) self.size = CGSize( - width: max(self.size.width, abs(self.items.map { $0.bounds.origin.x - $0.bounds.size.width }.min() ?? 0.0).rounded()), - height: max(self.size.height, abs(self.items.map { $0.bounds.origin.y - $0.bounds.size.height }.min() ?? 0.0).rounded()) + width: (self.items.map { $0.bounds.origin.x + $0.bounds.size.width }.max() ?? 0.0).rounded(), + height: (self.items.map { $0.bounds.origin.y + $0.bounds.size.height }.max() ?? 0.0).rounded() ) } } - -public struct GridPreferencesKey: PreferenceKey { - public static var defaultValue: GridPreferences = .init(items: []) - - public static func reduce(value: inout GridPreferences, nextValue: () -> GridPreferences) { - value.merge(with: nextValue()) - } -} diff --git a/Sources/Grid/Styles/Style/GridPreferencesKey.swift b/Sources/Grid/Styles/Style/GridPreferencesKey.swift new file mode 100644 index 0000000..8540562 --- /dev/null +++ b/Sources/Grid/Styles/Style/GridPreferencesKey.swift @@ -0,0 +1,10 @@ +import Foundation +import SwiftUI + +public struct GridPreferencesKey: PreferenceKey { + public static var defaultValue: GridPreferences = .init(items: []) + + public static func reduce(value: inout GridPreferences, nextValue: () -> GridPreferences) { + value.merge(with: nextValue()) + } +} diff --git a/Tests/GridTests/GridPreferencesTests.swift b/Tests/GridTests/GridPreferencesTests.swift new file mode 100644 index 0000000..579ec7c --- /dev/null +++ b/Tests/GridTests/GridPreferencesTests.swift @@ -0,0 +1,30 @@ +@testable import Grid +import XCTest + +class GridPreferencesTests: XCTestCase { + func testGridPreferencesMerge() { + var preferences = GridPreferences(items: [ + GridPreferences.Item(id: 1, bounds: .zero), + GridPreferences.Item(id: 2, bounds: .zero) + ]) + + let updatedPreferences = GridPreferences(items: [ + GridPreferences.Item(id: 1, bounds: .init(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 100, height: 100))), + GridPreferences.Item(id: 2, bounds: .init(origin: CGPoint(x: 50, y: 50), size: CGSize(width: 100, height: 100))) + ]) + + preferences.merge(with: updatedPreferences) + + XCTAssertEqual(preferences.size, CGSize(width: 150, height: 150)) + } + + func testGridPreferencesMergeWithZeroSizeAndItems() { + var preferences = GridPreferences(items: []) + XCTAssertEqual(preferences.size, .zero) + XCTAssertEqual(preferences.items, []) + + preferences.merge(with: GridPreferences(items: [])) + XCTAssertEqual(preferences.size, .zero) + XCTAssertEqual(preferences.items, []) + } +} diff --git a/Tests/GridTests/ModularGridStyleTests.swift b/Tests/GridTests/ModularGridStyleTests.swift new file mode 100644 index 0000000..5dc5b48 --- /dev/null +++ b/Tests/GridTests/ModularGridStyleTests.swift @@ -0,0 +1,62 @@ +@testable import Grid +import XCTest + +class ModularGridStyleTests: XCTestCase { + func testModularGridStyleWithOneRow() { + let style = ModularGridStyle(.vertical, columns: 4, rows: 1, spacing: 0) + + var preferences = GridPreferences(items: [ + GridPreferences.Item(id: 1, bounds: .zero), + GridPreferences.Item(id: 2, bounds: .zero), + GridPreferences.Item(id: 3, bounds: .zero), + GridPreferences.Item(id: 4, bounds: .zero) + ]) + + style.transform(preferences: &preferences, in: CGSize(width: 100, height: 50)) + + XCTAssertEqual(preferences, GridPreferences( + size: CGSize(width: 100, height: 50), + items: [ + GridPreferences.Item(id: 1, bounds: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 25, height: 50))), + GridPreferences.Item(id: 2, bounds: CGRect(origin: CGPoint(x: 25, y: 0), size: CGSize(width: 25, height: 50))), + GridPreferences.Item(id: 3, bounds: CGRect(origin: CGPoint(x: 50, y: 0), size: CGSize(width: 25, height: 50))), + GridPreferences.Item(id: 4, bounds: CGRect(origin: CGPoint(x: 75, y: 0), size: CGSize(width: 25, height: 50))) + ]) + ) + } + + func testModularGridStyleWithTwoRows() { + let style = ModularGridStyle(.vertical, columns: 2, rows: .fixed(50), spacing: 0) + + var preferences = GridPreferences(items: [ + GridPreferences.Item(id: 1, bounds: .zero), + GridPreferences.Item(id: 2, bounds: .zero), + GridPreferences.Item(id: 3, bounds: .zero), + GridPreferences.Item(id: 4, bounds: .zero) + ]) + + style.transform(preferences: &preferences, in: CGSize(width: 100, height: 100)) + + XCTAssertEqual(preferences, GridPreferences( + size: CGSize(width: 100, height: 100), + items: [ + GridPreferences.Item(id: 1, bounds: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 50, height: 50))), + GridPreferences.Item(id: 2, bounds: CGRect(origin: CGPoint(x: 50, y: 0), size: CGSize(width: 50, height: 50))), + GridPreferences.Item(id: 3, bounds: CGRect(origin: CGPoint(x: 0, y: 50), size: CGSize(width: 50, height: 50))), + GridPreferences.Item(id: 4, bounds: CGRect(origin: CGPoint(x: 50, y: 50), size: CGSize(width: 50, height: 50))) + ]) + ) + } + + func testModularGridStyleWithZeroSizeAndNoItems() { + let style = ModularGridStyle(.vertical, columns: 5, rows: 5, spacing: 0) + + var preferences = GridPreferences(items: []) + XCTAssertEqual(preferences.size, .zero) + + style.transform(preferences: &preferences, in: .zero) + XCTAssertEqual(preferences.size, .zero) + XCTAssertEqual(preferences.items, []) + XCTAssertEqual(preferences.items, []) + } +} diff --git a/Tests/GridTests/StaggeredGridStyleTests.swift b/Tests/GridTests/StaggeredGridStyleTests.swift new file mode 100644 index 0000000..035fd7c --- /dev/null +++ b/Tests/GridTests/StaggeredGridStyleTests.swift @@ -0,0 +1,27 @@ +@testable import Grid +import XCTest + +class StaggeredGridStyleTests: XCTestCase { + func testStaggeredGridStyleWithOneRow() { + let style = StaggeredGridStyle(.horizontal, tracks: 2, spacing: 0) + + var preferences = GridPreferences(items: [ + GridPreferences.Item(id: 1, bounds: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 100, height: 50))), + GridPreferences.Item(id: 2, bounds: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 100, height: 50))), + GridPreferences.Item(id: 3, bounds: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 100, height: 50))), + GridPreferences.Item(id: 4, bounds: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 100, height: 50))) + ]) + + style.transform(preferences: &preferences, in: CGSize(width: 200, height: 100)) + + XCTAssertEqual(preferences, GridPreferences( + size: CGSize(width: 200, height: 100), + items: [ + GridPreferences.Item(id: 1, bounds: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 100, height: 50))), + GridPreferences.Item(id: 2, bounds: CGRect(origin: CGPoint(x: 0, y: 50), size: CGSize(width: 100, height: 50))), + GridPreferences.Item(id: 3, bounds: CGRect(origin: CGPoint(x: 100, y: 0), size: CGSize(width: 100, height: 50))), + GridPreferences.Item(id: 4, bounds: CGRect(origin: CGPoint(x: 100, y: 50), size: CGSize(width: 100, height: 50))) + ]) + ) + } +}