The most powerful Grid container missed in SwiftUI

Related tags

Layout Grid
Overview

Grid

Grid view inspired by CSS Grid and written with SwiftUI


We are a development agency building phenomenal apps.




Twitter Build Status Version License Platform

Overview

Grid is a powerful and easy way to layout your views in SwiftUI:

Check out full documentation below.

Installation

CocoaPods

Grid is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'ExyteGrid'

Swift Package Manager

Grid is available through Swift Package Manager.

Add it to an existing Xcode project as a package dependency:

  1. From the File menu, select Swift Packages › Add Package Dependency…
  2. Enter "https://github.com/exyte/Grid" into the package repository URL text field

Requirements

  • iOS 14.0+ (the latest iOS 13 support is in v0.1.0)
  • MacOS 10.15+
  • Xcode 12+

Building from sources

git clone [email protected]:exyte/Grid.git
cd Grid/Example/
pod install
open Example.xcworkspace/

Documentation

1. Initialization

You can instantiate Grid in different ways:

  1. Just specify tracks and your views inside ViewBuilder closure:
Grid(tracks: 3) {
    ColorView(.blue)
    ColorView(.purple)
    ColorView(.red)
    ColorView(.cyan)
    ColorView(.green)
    ColorView(.orange)
}
  1. Use Range:
Grid(0..<6, tracks: 3) { _ in
    ColorView(.random)
}
  1. Use Identifiable enitites:
Grid(colorModels, tracks: 3) {
    ColorView($0)
}
  1. Use explicitly defined ID:
Grid(colorModels, id: \.self, tracks: 3) {
    ColorView($0)
}

2. Containers

ForEach

Inside ViewBuilder you also can use regular ForEach statement. There is no way to get KeyPath id value from the initialized ForEach view. Its inner content will be distinguished by views order while doing animations. It's better to use ForEach with Identifiable models or GridGroup created either with explicit ID value or Identifiable models to keep track of the grid views and their View representations in animations.

Grid(tracks: 4) {
    ColorView(.red)
    ColorView(.purple)
    
    ForEach(0..<4) { _ in
        ColorView(.black)
    }

    ColorView(.orange)
    ColorView(.green)
}

GridGroup

Number of views in ViewBuilder closure is limited to 10. It's impossible to obtain content views from regular SwiftUI Group view. To exceed that limit you could use GridGroup. Every view in GridGroup is placed as a separate grid item. Unlike the Group view any outer method modifications of GridView are not applied to the descendant views. So it's just an enumerable container. Also GridGroup could be created by Range<Int>, Identifiable models, by ID specified explicitly.

You can bind a view’s identity to the given single Hashable or Identifiable value also using GridGroup. This will produce transition animation to a new view with the same identity.

There is no way to use View's .id() modifier as inner ForEach view clears that value

You can use GridGroup.empty to define a content absence.

Examples:

var arithmeticButtons: GridGroup {
    GridGroup {
        CalcButton(.divide)
        CalcButton(.multiply)
        CalcButton(.substract)
        CalcButton(.equal)
    }
}
var arithmeticButtons: GridGroup {
    let operations: [MathOperation] =
        [.divide, .multiply, .substract, .add, .equal]
	
    return GridGroup(operations, id: \.self) {
        CalcButton($0)
    }
}
var arithmeticButtons: GridGroup {
    let operations: [MathOperation] =
        [.divide, .multiply, .substract, .add, .equal]
	
    return GridGroup {
        ForEach(operations, id: \.self) {
            CalcButton($0)
        }
    }
}
var arithmeticButtons: GridGroup {
    let operations: [MathOperation] =
        [.divide, .multiply, .substract, .add, .equal]
    return GridGroup(operations, id: \.self) { 
         CalcButton($0)
    }
}
var arithmeticButtons: GridGroup {
    let operations: [MathOperation] =
        [.divide, .multiply, .substract, .add, .equal]
    return GridGroup(operations, id: \.self) { 
         CalcButton($0)
    }
}
Grid {
...
    GridGroup(MathOperation.clear) {
        CalcButton($0)
    }
}

3. Track sizes

There are 3 types of track sizes that you could mix with each other:

Fixed-sized track:

.pt(N) where N - points count.

Grid(tracks: [.pt(50), .pt(200), .pt(100)]) {
    ColorView(.blue)
    ColorView(.purple)
    ColorView(.red)
    ColorView(.cyan)
    ColorView(.green)
    ColorView(.orange)
}

Content-based size: .fit

Defines the track size as a maximum of the content sizes of every view in track

Grid(0..<6, tracks: [.fit, .fit, .fit]) {
    ColorView(.random)
        .frame(maxWidth: 50 + 15 * CGFloat($0))
}

Pay attention to limiting a size of views that fills the entire space provided by parent and Text() views which tend to draw as a single line.

Flexible sized track: .fr(N)

Fr is a fractional unit and .fr(1) is for 1 part of the unassigned space in the grid. Flexible-sized tracks are computed at the very end after all non-flexible sized tracks (.pt and .fit). So the available space to distribute for them is the difference of the total size available and the sum of non-flexible track sizes.

Grid(tracks: [.pt(100), .fr(1), .fr(2.5)]) {
    ColorView(.blue)
    ColorView(.purple)
    ColorView(.red)
    ColorView(.cyan)
    ColorView(.green)
    ColorView(.orange)
}

Also, you could specify just an Int literal as a track size. It's equal to repeating .fr(1) track sizes:

Grid(tracks: 3) { ... }

is equal to:

Grid(tracks: [.fr(1), .fr(1), .fr(1)]) { ... }

4. Grid cell background and overlay

When using non-flexible track sizes it's possible that the extra space to be allocated will be greater than a grid item is able to take up. To fill that space you could use .gridCellBackground(...) and gridCellOverlay(...) modifiers.

See Content mode and Spacing examples.


5. Spans

Every grid view may span across the provided number of grid tracks. You can achieve it using .gridSpan(column: row:) modifier. The default span is 1.

View with span >= 2 that spans across the tracks with flexible size doesn't take part in the sizes distribution for these tracks. This view will fit to the spanned tracks. So it's possible to place a view with unlimited size that spans tracks with content-based sizes (.fit)

Grid(tracks: [.fr(1), .pt(150), .fr(2)]) {
    ColorView(.blue)
        .gridSpan(column: 2)
    ColorView(.purple)
        .gridSpan(row: 2)
    ColorView(.red)
    ColorView(.cyan)
    ColorView(.green)
        .gridSpan(column: 2, row: 3)
    ColorView(.orange)
    ColorView(.magenta)
        .gridSpan(row: 2)
}

Spanning across tracks with different size types:

var body: some View {
    Grid(tracks: [.fr(1), .fit, .fit], spacing: 10) {
        VCardView(text: placeholderText(),
                  color: .red)
        
        VCardView(text: placeholderText(length: 30),
                  color: .orange)
            .frame(maxWidth: 70)
        
        VCardView(text: placeholderText(length: 120),
                  color: .green)
            .frame(maxWidth: 100)
            .gridSpan(column: 1, row: 2)
        
        VCardView(text: placeholderText(length: 160),
                  color: .magenta)
            .gridSpan(column: 2, row: 1)
        
        VCardView(text: placeholderText(length: 190),
                  color: .cyan)
            .gridSpan(column: 3, row: 1)
    }
}

6. Starts

For every view you are able to set explicit start position by specifying a column, a row or both. View will be positioned automatically if there is no start position specified. Firstly, views with both column and row start positions are placed. Secondly, the auto-placing algorithm tries to place views with either column or row start position. If there are any conflicts - such views are placed automatically and you see warning in the console. And at the very end views with no explicit start position are placed.

Start position is defined using .gridStart(column: row:) modifier.

Grid(tracks: [.pt(50), .fr(1), .fr(1.5), .fit]) {
    ForEach(0..<6) { _ in
        ColorView(.black)
    }
    
    ColorView(.brown)
        .gridSpan(column: 3)
    
    ColorView(.blue)
        .gridSpan(column: 2)
    
    ColorView(.orange)
        .gridSpan(row: 3)
    
    ColorView(.red)
        .gridStart(row: 1)
        .gridSpan(column: 2, row: 2)
    
    ColorView(.yellow)
        .gridStart(row: 2)
    
    ColorView(.purple)
        .frame(maxWidth: 50)
        .gridStart(column: 3, row: 0)
        .gridSpan(row: 9)
    
    ColorView(.green)
        .gridSpan(column: 2, row: 3)
    
    ColorView(.cyan)
    
    ColorView(.gray)
        .gridStart(column: 2)
}

7. Flow

Grid has 2 types of tracks. The first one is where you specify track sizes - the fixed one. Fixed means that a count of tracks is known. The second one and orthogonal to the fixed is growing tracks type: where your content grows. Grid flow defines the direction where items grow:

Rows

Default. The number of columns is fixed and defined as track sizes. Grid items are placed moving between columns and switching to the next row after the last column. Rows count is growing.

Columns

The number of rows is fixed and defined as track sizes. Grid items are placed moving between rows and switching to the next column after the last row. Columns count is growing.

Grid flow could be specified in a grid constructor as well as using .gridFlow(...) grid modifier. The first option has more priority.

struct ContentView: View {
    @State var flow: GridFlow = .rows
    
    var body: some View {
        VStack {
            if self.flow == .rows {
                Button(action: { self.flow = .columns }) {
                    Text("Flow: ROWS")
                }
            } else {
                Button(action: { self.flow = .rows }) {
                    Text("Flow: COLUMNS")
                }
            }
            
            Grid(0..<15, tracks: 5, flow: self.flow, spacing: 5) {
                ColorView($0.isMultiple(of: 2) ? .black : .orange)
                    .overlay(
                        Text(String($0))
                            .font(.system(size: 35))
                            .foregroundColor(.white)
                )
            }
            .animation(.default)
        }
    }
}

8. Content mode

There are 2 kinds of content modes:

Scroll

In this mode the inner grid content is able to scroll to the growing direction. Grid tracks that orthogonal to the grid flow direction (growing) are implicitly assumed to have .fit size. This means that their sizes have to be defined in the respective dimension.

Grid content mode could be specified in a grid constructor as well as using .gridContentMode(...) grid modifier. The first option has more priority.

Rows-flow scroll:

struct VCardView: View {
   let text: String
   let color: UIColor
   
   var body: some View {
       VStack {
           Image("dog")
               .resizable()
               .aspectRatio(contentMode: .fit)
               .cornerRadius(5)
               .frame(minWidth: 100, minHeight: 50)
           
           Text(self.text)
               .layoutPriority(.greatestFiniteMagnitude)
       }
       .padding(5)
       .gridCellBackground { _ in
           ColorView(self.color)
       }
       .gridCellOverlay { _ in
           RoundedRectangle(cornerRadius: 5)
               .strokeBorder(Color(self.color.darker()),
                             lineWidth: 3)
       }
   }
}

struct ContentView: View {
   var body: some View {
       Grid(tracks: 3) {
           ForEach(0..<40) { _ in
               VCardView(text: randomText(), color: .random)
                   .gridSpan(column: self.randomSpan)
           }
       }
       .gridContentMode(.scroll)
       .gridPacking(.dense)
       .gridFlow(.rows)
   }
   
   var randomSpan: Int {
       Int(arc4random_uniform(3)) + 1
   }
}
Columns-flow scroll:

struct HCardView: View {
   let text: String
   let color: UIColor

   var body: some View {
       HStack {
           Image("dog")
               .resizable()
               .aspectRatio(contentMode: .fit)
               .cornerRadius(5)
           
           Text(self.text)
               .frame(maxWidth: 200)
       }
       .padding(5)
       .gridCellBackground { _ in
           ColorView(self.color)
       }
       .gridCellOverlay { _ in
           RoundedRectangle(cornerRadius: 5)
               .strokeBorder(Color(self.color.darker()),
                             lineWidth: 3)
       }
   }
}

struct ContentView: View {
   var body: some View {
       Grid(tracks: 3) {
           ForEach(0..<8) { _ in
               HCardView(text: randomText(), color: .random)
                   .gridSpan(row: self.randomSpan)
           }
       }
       .gridContentMode(.scroll)
       .gridFlow(.columns)
       .gridPacking(.dense)
   }
   
   var randomSpan: Int {
       Int(arc4random_uniform(3)) + 1
   }
}

Fill

Default. In this mode, grid view tries to fill the entire space provided by the parent view with its content. Grid tracks that orthogonal to the grid flow direction (growing) are implicitly assumed to have .fr(1) size.

@State var contentMode: GridContentMode = .scroll

var body: some View {
    VStack {
        self.modesPicker
        
        Grid(models, id: \.self, tracks: 3) {
            VCardView(text: $0.text, color: $0.color)
                .gridSpan($0.span)
        }
        .gridContentMode(self.contentMode)
        .gridFlow(.rows)
        .gridAnimation(.default)
    }
}

9. Packing

Auto-placing algorithm could stick to one of two strategies:

Sparse

Default. The placement algorithm only ever moves “forward” in the grid when placing items, never backtracking to fill holes. This ensures that all of the auto-placed items appear “in order”, even if this leaves holes that could have been filled by later items.

Dense

Attempts to fill in holes earlier in the grid if smaller items come up later. This may cause items to appear out-of-order, when doing so would fill in holes left by larger items.

Grid packing could be specified in a grid constructor as well as using .gridPacking(...) grid modifier. The first option has more priority.

Example:

@State var gridPacking = GridPacking.sparse

var body: some View {
    VStack {
        self.packingPicker

        Grid(tracks: 4) {
            ColorView(.red)
            
            ColorView(.black)
                .gridSpan(column: 4)
            
            ColorView(.purple)
  
            ColorView(.orange)
            ColorView(.green)
        }
        .gridPacking(self.gridPacking)
        .gridAnimation(.default)
    }
}

10. Spacing

There are several ways to define the horizontal and vertical spacings between tracks:

  • Using Int literal which means equal spacing in all directions:
Grid(tracks: 4, spacing: 5) { ... } 
  • Using explicit init
Grid(tracks: 4, spacing: GridSpacing(horizontal: 10, vertical: 5)) { ... } 
  • Using array literal:
Grid(tracks: 4, spacing: [10, 5]) { ... } 

Example:

@State var vSpacing: CGFloat = 0
@State var hSpacing: CGFloat = 0

var body: some View {
    VStack {
        self.sliders
        
        Grid(tracks: 3, spacing: [hSpacing, vSpacing]) {
            ForEach(0..<21) {
                //Inner image used to measure size
                self.image
                    .aspectRatio(contentMode: .fit)
                    .opacity(0)
                    .gridSpan(column: max(1, $0 % 4))
                    .gridCellOverlay {
                        //This one is to display
                        self.image
                            .aspectRatio(contentMode: .fill)
                            .frame(width: $0?.width, 
			           height: $0?.height)
                            .cornerRadius(5)
                            .clipped()
                            .shadow(color: self.shadowColor, 
			            radius: 10, x: 0, y: 0)
                }
            }
        }
        .background(self.backgroundColor)
        .gridContentMode(.scroll)
        .gridPacking(.dense)
    }
}

11. Alignment

.gridItemAlignment

Use this to specify the alignment for a specific single grid item. It has higher priority than gridCommonItemsAlignment

.gridCommonItemsAlignment

Applies to every item as gridItemAlignment, but doesn't override its individual gridItemAlignment value.

.gridContentAlignment

Applies to the whole grid content. Takes effect when content size is less than the space available for the grid.

Example:

struct SingleAlignmentExample: View {
  var body: some View {
    Grid(tracks: 3) {
      TextCardView(text: "Hello", color: .red)
        .gridItemAlignment(.leading)

      TextCardView(text: "world", color: .blue)
    }
    .gridCommonItemsAlignment(.center)
    .gridContentAlignment(.trailing)
  }
}

struct TextCardView: View {
  let text: String
  let color: UIColor
  var textColor: UIColor = .white

  var body: some View {
    Text(self.text)
      .foregroundColor(Color(self.textColor))
      .padding(5)
      .gridCellBackground { _ in
        ColorView(color)
      }
      .gridCellOverlay { _ in
        RoundedRectangle(cornerRadius: 5)
          .strokeBorder(Color(self.color.darker()),
                        lineWidth: 3)
      }
  }
}

12. Animations

You can define a specific animation that will be applied to the inner ZStack using .gridAnimation() grid modifier.
By default, every view in the grid is associated with subsequent index as it's ID. Hence SwiftUI relies on the grid view position in the initial and final state to perform animation transition. You can associate a specific ID to a grid view using ForEach or GridGroup initialized by Identifiable models or by explicit KeyPath as ID to force an animation to perform in the right way.

There is no way to get KeyPath id value from the initialized ForEach view. Its inner content will be distinguished by views order while doing animations. It's better to use ForEach with Identifiable models or GridGroup created either with explicit ID value or Identifiable models to keep track of the grid views and their View representations in animations.


13. Caching

It's possible to cache grid layouts through the lifecycle of Grid.

Supported for iOS only

Grid caching could be specified in a grid constructor as well as using .gridCaching(...) grid modifier. The first option has more priority.

In memory cache

Default. Cache is implemented with the leverage of NSCache. It will clear all the cached layouts on the memory warning notification.

No cache

No cache is used. Layout calculations will be executed at every step of Grid lifecycle.


14. Conditional statements / @GridBuilder

Starting with Swift 5.3 we can use custom function builders without any issues. That gives us:

  • Full support of if/if else, if let/if let else, switch statements within the Grid and GridGroup bodies.

  • A better way to propagate view ID from nested GridGroup and ForEach

  • An ability to return heterogeneous views from functions and vars using @GridBuilder attribute and some View retrun type:

@GridBuilder
func headerSegment(flag: Bool) -> some View {
    if flag {
        return GridGroup { ... }
    else {
        return ColorView(.black)
    }
}

Release notes:

v1.4.0:
  • adds gridItemAlignment modifier to align per item
  • adds gridCommonItemsAlignment modifier to align all items
  • adds gridContentAlignment modifier to align the whole grid content
Previous releases
v1.3.1.beta:
  • adds gridAlignment modifier to align per item
v1.2.1.beta:
  • adds gridCommonItemsAlignment modifier to align all items in Grid
v1.1.1.beta:
  • adds WidgetKit support by conditionally rendering ScrollView
v1.1.0:
  • adds MacOS support
v1.0.1:
  • adds full support of conditional statements
  • adds @GridBuilder function builder
v0.1.0:
  • adds layout caching
  • adds GridGroup init using a single Identifiable or Hashable value
v0.0.3:
  • fixes any issues when Grid is conditionally presented
  • fixes wrong grid position with scrollable content after a device rotation
  • fixes "Bound preference ** tried to update multiple times per frame" warnings in iOS 14 and reduces them in iOS 13
  • simplifies the process of collecting grid preferences under the hood
v0.0.2
  • added support for Swift Package Manager

Roadmap:

  • add WidgetKit example
  • add alignment per tracks
  • add regions or settings for GridGroup to specify position
  • dual dimension track sizes (grid-template-rows, grid-template-columns).
  • grid-auto-rows, grid-auto-columns
  • improve dense placement algorithm
  • ? grid min/ideal sizes
  • ? landscape/portrait layout
  • ? calculate layout in background thread
  • add GridIdentified-like item to track the same Views in animations
  • support if clauses using function builder
  • add GridGroup
  • grid item explicit row and/or column position
  • different spacing for rows and columns
  • intrinsic sized tracks (fit-content)
  • forEach support
  • dense/sparse placement algorithm
  • add horizontal axis
  • init via Identifiable models
  • scrollable content

License

Grid is available under the MIT license. See the LICENSE file for more info.

Comments
  • How do you refresh the grid?

    How do you refresh the grid?

    I have a macOS app that I'm adding Grid to, but when I pass in new data, the grid is not refreshed.

    However, if I switch to using LazyVGrid, it does refresh. So I know the data is getting to the SwiftUI view. I'm integrating Grid using an NSHostingController and I'm monitoring changes to my object using @ObjectBinding. Anything outside the Grid will update when I change objects, but anything within the Grid does not.

    Think of a master/detail view. The master is an AppKit NSTableView that shows a list of records. The detail view is my SwiftUI view that's using Grid to display a list of fields on screen. But when I switch records, the SwiftUI view's Grid never refreshes.

    opened by brendand 11
  • Provide alignment option for cell

    Provide alignment option for cell

    I am still a SwiftUI newbie for sure, but I just could not figure out how to easily align cell contents, to change from centered.

    For example, in below I would like "A" to right align in the cell:

    struct GridIssue2View: View {
        var body: some View {
            Grid(tracks: [
                .fit, // label
                .fit, // value
            ], spacing: 5)
            {
                Text("A")
                Text("B")
                Text("CCCCC")
                Text("D")
            }
            .frame(width: 100, height: 50)
            .border(Color.blue, width: 1)
        }
    }
    

    image I tried swapping out Text("A") with:

                HStack
                {
                    Spacer()
                    Text("A")
                }
    

    Thinking that might push the A over. But it yielded:

    image

    Thank you for any possible help!

    enhancement 
    opened by t9mike 7
  • Grid disappears when placed inside a ScrollView

    Grid disappears when placed inside a ScrollView

    When the grid is placed inside a scrollview, the grid just disappears.

    import SwiftUI
    import ExyteGrid
    
    struct ContentView: View {
        
        var body: some View {
            
            ScrollView {
                Grid(array, id: \.id, tracks: [.fit, .fit], spacing: 20) { user in
                    VStack {
                        Text(user.name)
                            .font(.largeTitle)
                        
                        Text(user.image)
                            .font(.largeTitle)
                            .background(Color.orange)
                    }
                }
            }
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    
    struct User: Identifiable {
        let id = UUID().uuidString
        let name: String
        let image: String
    }
    
    var array = [
        User(name: "Hello", image: "😀"),
        User(name: "No one here", image: "😆"),
        User(name: "Hello", image: "😀"),
        User(name: "No one here", image: "😆"),
        User(name: "Hello", image: "😀"),
        User(name: "No one here", image: "😆"),
        User(name: "Hello", image: "😀"),
        User(name: "No one here", image: "😆"),
        User(name: "Hello", image: "😀"),
        User(name: "No one here", image: "😆"),
    ]
    
    bug confirmed 
    opened by oalansari82 5
  • Resizable Image with .aspectRatio(contentMode: .fill) not correctly displayed

    Resizable Image with .aspectRatio(contentMode: .fill) not correctly displayed

    When using a resizable Image with .aspectRatio(contentMode: .fill), the image may not be correctly displayed.

    Here's a code example to reproduce the issue:

    Grid(tracks: 3) {
        ForEach(0 ..< 7) { i in
            Image("ExampleImage")
            .resizable()
            .aspectRatio(contentMode: .fill)
        }
    }
    .gridContentMode(.scroll)
    

    With ForEach(0 ..< 1), the image fills the whole width of the grid container (instead of just one column).

    The more rows are added, the more zoomed in the images become.

    With 10 rows, only a single pixel of the image is visible, filling the entire screen.

    opened by palle-k 4
  • Support macOS

    Support macOS

    It would be great if Grid could target support for macOS. It seems to support Catalyst, but that is not good enough for a native Mac app.

    Screenshot 2020-09-19 at 20 24 12

    I took a stab at it and was able to add macOS support pretty easily. Unfortunately Xcode messes with so many project settings (and I don't know your preferred setup/naming) so it would probably be easier if you just addressed this on your end.

    Steps to add Mac support:

    1. Add macOS Framework target
    2. Add all source files
    3. Mac's dont have didReceiveMemoryWarningNotification. Find another solution for the layout cache.
    4. Update Package.swift to add platform
    5. Address some minor issues with examples using UIColor etc.
    6. Done!
    opened by OskarGroth 3
  • Need to explicitly assign height if it's conditionally rendered

    Need to explicitly assign height if it's conditionally rendered

    If the grid view is conditionally rendered, we have to manually assign its height.

    Here is code snippets

    // Use Grid in child view
    
    struct RaceRunnerForm: View {
        @State private var isShownFullForm = false
        var body: some View {
            VStack(alignment: .leading, spacing: 5) {
                Text("Comments").bold()
                Divider()
                
                Grid(0..<formItemCount, tracks: 2, spacing: 10) { index in
                    HStack(alignment:.center) {
                        Text("\(self.formLabelArray[index])")
                        Spacer()
                        Text(self.formDataArray[index])
                    }
                    .frame(height: 37)
                }.padding(.leading, -10).padding(.trailing, -10)
                 .frame(height: 300) // have to assign height explictly
            }
        }
    }
    
    // Call from the parent view
    
    @State private var isExpanded = false  // `Conditionally rendering`
    var body: some View {
            VStack {
                HStack {
                    VStack(spacing: 10) {
                    VStack(alignment: .leading) {
                        Text(entrant.name).bold()
                    }
                if isExpanded { 
                    RaceRunnerForm()
                }
            }
        }
    bug confirmed 
    opened by ghost 3
  • Idea - No issue

    Idea - No issue

    Like in the Fitbit app it would be nice to make them re-adjustable - going to use your library for showing dynamic data so would be an appreciated feature if you could move around the "block" on the Grid.

    Great library, thanks!

    enhancement 
    opened by SebastianBO 3
  • can not add views with if state of a @State boolean value

    can not add views with if state of a @State boolean value

    Hi, Adding view(ex: button ) with a if statement of a @State boolean value is not working inside of Grid with forEach. What can I do about it? Any idea?

    opened by yildirimatcioglu 2
  • WidgetKit issues due to ScrollView

    WidgetKit issues due to ScrollView

    Hello. I using Grid for some iOS14 widgets. I am running into this:

    screenshot_2841

    SwiftUI is throwing up a warning per https://stackoverflow.com/questions/63135404/why-do-some-views-appear-as-a-red-no-entry-sign-in-widgets. The Grid's ScrollView was the issue.

    I believe ScrollView would only be needed in .gridContentMode(.scroll) mode. As a quick hack I commented out the ScrollView in Grid and all seems good.

    I am fairly new to both Swift and SwiftUI as assume you would have a much more elegant real fix than I could provide. I won't need scrollable maybe ever for my project. But I could take a crack at a "real" fix.

    Love the Grid! Thank you!!

    opened by t9mike 2
  • NavigationBarItems: invalid input index: 2

    NavigationBarItems: invalid input index: 2

    When attempting to render a scrollable grid in a navigation view with a navigation bar item, I get the following error: " precondition failure: invalid input index: 2". I've been able to run it successfully without the navigation bar item and still with a navigation bar title, but as soon as I add the item back in it fails.

    I can render the grid view without being scrollable and with the navigation bar item there and it works as expected.

    Here is my view with the grid:

    struct ListView: View {
        
        private let elements = ElementsModel()
        @State private var showSheet = false
        
        var body: some View {
            NavigationView {
                Grid(elements.elements, tracks: 3) {element in
                    SquareView(element: element, showElementName: true)
                }
                .gridContentMode(.scroll)
                .edgesIgnoringSafeArea(.all)
                .navigationBarTitle("🧪 Elements")
                .navigationBarItems(trailing:
                    Text("Test")
                )
            }
        }
    }
    

    Here is the SquareView that is inside the body:

    struct SquareView: View {
        
        let element: Element
        let showElementName: Bool
        
        var body: some View {
            VStack {
                Text("\(element.number)")
                    .frame(maxWidth: .infinity, alignment: .leading)
                
                Text(element.symbol)
                    .bold()
                    .font(.system(size: 50))
                
                Group {
                    if showElementName {
                        Text(String(element.name))
                            .bold()
                    } else {
                        if element.atomicMass != nil {
                            Text(String(element.atomicMass!))
                        }
                    }
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .modifier(OneLineText())
            .aspectRatio(1, contentMode: .fit)
            .foregroundColor(.white)
            .padding(5)
            .background(elementColor[element.category])
        }
    }
    

    Please let me know if there are any debugging tasks I can do to provide more information

    opened by lorenzolewis 2
  • Change cell span dynamically

    Change cell span dynamically

    Hi,

    I was wondering if it is possible to change the column span of a cell dynamically as you change the flow in the documentation: https://github.com/exyte/Grid#columns?

    opened by codertimu 1
  • TextEditor with multiple columns?

    TextEditor with multiple columns?

    Awesome library guys!

    Would it be possible to use the Grid to make a TextEditor with multiple columns, like on Apple Pages for example?

    Screenshot 2022-09-30 at 12 02 17

    I tried bellow, but have no idea how to make this work...

    import SwiftUI
    import ExyteGrid
    
    
    struct ContentView: View {
    
        let inputText = "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"
        
        var body: some View {
    
            Grid(tracks: 2) {
              Text(inputText)
            }
        }
    }
    
    opened by alelordelo 0
  • zIndex for Grid Items

    zIndex for Grid Items

    Issue

    Scaling a view within the Grid causes incorrect overlay.

    Description

    I attempted to manipulate the zIndex by wrapping my Grid Item View in a ZStack and changing the zIndex whenever that particular view scaled.

    However, it does not appear to have any effect.

    Details

    Here is a sample code snippet of my Grid implementation:

        var body: some View {
            Grid(tracks: layout.tracks) {
                ForEach(viewModel.items, id: \.self) { item in
                    ZStack {
                        LayoutItem(item, action: { action in
                            viewModel.handle(action, for: item)
                        })
                    }
                    .zIndex(item.zIndex)
                    .gridSpan(column: item.type.column,
                              row: item.type.row)
                }
            }
            .gesture(drag)
            .coordinateSpace(name: GridLayout.NAME_SPACE)
        }
    

    Here is a code snippet of my toggle scale logic

        public func handle(drag location: CGPoint?) {
            for var item in items {
                var inBounds = false
                var scale: GridLayoutItemScale = .normal
                var zIndex: Double = 0
                if let location = location {
                    inBounds = item.bounds.contains(location)
                    scale = inBounds ? .large : .normal
                    zIndex = inBounds ? 1 : 0
                }
                item.scale = scale
                item.zIndex = zIndex
                setItem(item)
            }
        }
    

    Demo

    Demo

    Conclusion

    I looked through the documentation and wasn't able to find anything specific to zIndex manipulation.

    Is it possible to alter the zIndex for Grid Items within the Grid to avoid bottom overlay?

    If not, it would be a great addition to this package! I've been using it a lot and it is FANTASTIC!

    opened by gabrielleyva 0
  • Only show one item when view appear, click to restore

    Only show one item when view appear, click to restore

    Hello, I use a @State variable to control display Grid, like this:

    if(unit == "w"){
        Grid(tracks: 3) {
            ForEach(weekSelectArr.indices){item in
                Toggle(isOn: self.$weekSelectArr[item]) {
                    Text(weekdays[item])
                        .frame(height:50)
                }.toggleStyle(GridToggleStyle())
            }
        }.frame(height:200)
    }
    

    When the view appear, it shows like this: only show one item. image

    When I click the item, it come back into grid: image

    Their parent view is a Form->Section.

    opened by AntoniotheFuture 2
  • Items overlap

    Items overlap

    I have a problem where the elements of the grid are overlapping. My code is:

    var body: some View {
         ScrollView {
                VStack(alignment: .leading) {
                    Group {
                        Text("Letters en woorden")
                            .font(.title)
                        Grid(tracks: 3) {
                            Text("_eererer_")
                            Text("_eererer__")
                            Text("_eererer_")
                            Text("_eererer_")
                            Text("_eererer_")
                            Text("_eererer_")
                        }
                    // ...
                    }
                }.padding(.bottom)
         }.navigationTitle("Morse")
           .padding(.horizontal)
    }
    

    This is how my view look Schermafbeelding 2021-10-16 om 13 06 56

    opened by Jomy10 2
  • Performance issue when scroll has 1000 items and more

    Performance issue when scroll has 1000 items and more

    Here is my code:

    Grid(store.state.categories, id: \.id, tracks: 2) { category in
          SectionView( text: category.name)
    }.gridContentMode(.scroll)
    

    When I do it with List there is no issue, no lags. Would be happy if you can let me know if there is something that I missed.

    Thanks

    opened by davut 1
Owner
Exyte
Exyte
SwiftUI Grid layout with custom styles

SwiftUI Grid SwiftUI Grid view layout with custom styles. Features ZStack based layout Vertical and horizontal scrolling Supports all apple platforms

SpaceNation 928 Dec 15, 2022
Simple Catalyst example (Mac idiom) of a grid-based app populated with photos, with dynamic cell layout switching

Catalyst Photo Grid Simple Catalyst example (Mac idiom) of a grid-based app populated with photos that can animate its cells between two different lay

Steven Troughton-Smith 56 Nov 14, 2022
Try to create my own DI container

Basic Dependecy Injection Simple solution for DI Dependecy registration Registration strong dependency Registration weak dependency Registration with

Evgeny Bayan 2 Apr 20, 2022
Using Kiva's free API, the demo app retrieves its most recent fundraising loans

KivaLoan Kiva is a non-profit organization with a mission to connect people through lending to alleviate poverty. Using Kiva's free API, the demo app

DaryliOSdev 0 Nov 28, 2021
Powerful autolayout framework, that can manage UIView(NSView), CALayer and not rendered views. Not Apple Autolayout wrapper. Provides placeholders. Linux support.

CGLayout Powerful autolayout framework, that can manage UIView(NSView), CALayer and not rendered views. Has cross-hierarchy coordinate space. Implemen

Koryttsev Denis 45 Jun 28, 2022
A powerful Swift programmatic UI layout framework.

Build dynamic and beautiful user interfaces like a boss, with Swift. Neon is built around how user interfaces are naturally and intuitively designed.

Mike 4.6k Dec 26, 2022
The ultimate API for iOS & OS X Auto Layout — impressively simple, immensely powerful. Objective-C and Swift compatible.

The ultimate API for iOS & OS X Auto Layout — impressively simple, immensely powerful. PureLayout extends UIView/NSView, NSArray, and NSLayoutConstrai

PureLayout 7.6k Jan 6, 2023
Intuitive and powerful Auto Layout library

Align introduces a better alternative to Auto Layout anchors. Semantic. Align APIs focus on your goals, not the math behind Auto Layout constraints. P

Alexander Grebenyuk 338 Oct 18, 2022
⚓️ Declarative, extensible, powerful Auto Layout

EasyAnchor ❤️ Support my apps ❤️ Push Hero - pure Swift native macOS application to test push notifications PastePal - Pasteboard, note and shortcut m

Khoa 449 Nov 10, 2022
What's New In SwiftUI for iOS 16 - Xcode 14 - SwiftUI 4.0

SwiftUI4 What's New In SwiftUI for iOS 16 - Xcode 14 - SwiftUI 4.0 (Work in progress....) Swift Charts Presentation Detents(Half Sheet & Small Sheets)

Sebastiao Gazolla Jr 3 Oct 23, 2022
Flow layout / tag cloud / collection view in SwiftUI.

SwiftUIFlowLayout A Flow Layout is a container that orders its views sequentially, breaking into a new "line" according to the available width of the

Gordan Glavaš 115 Dec 28, 2022
NStack is a SwiftUI view that allows you to hoist navigation state into a Coordinator

An NStack allows you to manage SwiftUI navigation state with a single stack property. This makes it easy to hoist that state into a high-level view, such as a coordinator. The coordinator pattern allows you to write isolated views that have zero knowledge of their context within the navigation flow of an app.

John Patrick Morgan 469 Dec 27, 2022
Half modal view for SwiftUI

ResizableSheet ResizableSheeet is a half modal view library for SwiftUI. You can easily implement a half modal view. Target Swift5.5 iOS14+ Installati

matsuji 76 Dec 16, 2022
✨ Super sweet syntactic sugar for SwiftUI.View initializers.

ViewCondition ✨ Super sweet syntactic sugar for SwiftUI.View initializers. At a Glance struct BorderTextView: View { var color: Color? @ViewBuild

Yoon Joonghyun 76 Dec 17, 2022
Expose layout margins and readable content width to SwiftUI's Views

SwiftUI Layout Guides This micro-library exposes UIKit's layout margins and readable content guides to SwiftUI. Usage Make a view fit the readable con

Thomas Grapperon 26 Dec 23, 2022
SwiftUI package to present a Bottom Sheet interactable view with the desired Detents. Also known as Half sheet.

BottomSheetSUI BottomSheetSUI is a package that gives you the ability to show a Bottom sheet intractable, where you can add your own SwiftUI view. You

Aitor Pagán 8 Nov 28, 2022
This app presents few examples for common patterns using purer SwiftUI code

VIPERtoSwiftUI GOAL This app presents few examples for common patterns using purer (from authors experience) SwiftUI code. LITERATURE https://www.appy

Tomislav Gelešić 0 Dec 19, 2021
Apple provides us two ways to use UIKit views in SwiftUI

RepresentableKit Apple provides us two ways to use UIKit views in SwiftUI: UIVie

YUMEMI Inc. 43 Dec 26, 2022
An experiment creating a particle emitter using the new TimelineView and Canvas views in SwiftUI

Particle Emitter An experiment creating a particle emitter using the new Timelin

Emilio Peláez 8 Nov 11, 2022