UICollectionView to SwiftUI

CollectionViewer

The package wraps the UICollectionView, providing a convenient interaction with SwiftUI.

Key features:

  • Reactive connection with incoming data. You don’t have to worry about updating the cells, just change the incoming array.
  • Automatic calculation of the size of displayed views.
  • Data buffering – to improve performance
  • Ability to update content with a native gesture

Designed to display static content (lists, tiles, etc.) that will not change size after display.

Package Goals

  • The need for an analogue of the LazyVGrid with better performance and correct memory release on iOS 15 and below.
  • Update gesture support on iOS 14

Warning

The package is in beta stage. It may and most likely contains errors. Use with caution.

Examples of using

Fully automatic use

struct ExampleView: View {
    @State var data: [Int] = (0...5000).map{$0}
    
    var body: some View {
        CollectionView(self.data) { index, element in
            VStack{
                Text("Index: \(index)")
                Text("Element: \(element)")
            }
            .background(Color.red)
        }
    }
}

Define columns

struct ExampleView: View {
    @State var data: [Int] = (0...5000).map{$0}
    
    var body: some View {
        CollectionView(self.data) { index, element in
            VStack{
                Text("Index: \(index)")
                Text("Element: \(element)")
            }
            .background(Color.red)
        }
        .gridColumns(1)
    }
}

struct ExampleView: View {
    @State var data: [Int] = (0...5000).map{$0}
    
    var body: some View {
        CollectionView(self.data) { index, element in
            VStack{
                Text("Index: \(index)")
                Text("Element: \(element)")
            }
            .background(Color.red)
        }
        .gridColumns(2)
    }
}

Fill empty space with a modifier

struct ExampleView: View {
    @State var data: [Int] = (0...5000).map{$0}
    
    var body: some View {
        CollectionView(self.data) { index, element in
            VStack{
                Text("Index: \(index)")
                Text("Element: \(element)")
            }
            .fillSpace(.horizontal)
            .background(Color.red)
        }
        .gridColumns(1)
    }
}

struct ExampleView: View {
    @State var data: [Int] = (0...5000).map{$0}
    
    var body: some View {
        CollectionView(self.data) { index, element in
            VStack{
                Text("Index: \(index)")
                Text("Element: \(element)")
            }
            .fillSpace(.horizontal)
            .background(Color.red)
        }
        .gridColumns(2)
    }
}

Manage sizes

struct ExampleView: View {
    @State var data: [Int] = (0...5000).map{$0}
    
    var body: some View {
        CollectionView(self.data) { index, element in
            VStack{
                Text("Index: \(index)")
                Text("Element: \(element)")
            }
            .fillSpace(.horizontal)
            .background(Color.red)
        }
        .itemSize(.init(width: 70, height: 70))
    }
}

Update your incoming data

struct ExampleView: View {
    @State var data: [Int] = (0...10).map{$0}
    var body: some View {
        CollectionView(self.data) { index, element in
            VStack{
                Text("Index: \(index)")
                Text("Element: \(element)")
            }
            .fillSpace(.horizontal)
            .background(Color.red)
            .onAppear{
                if index == 5 {
                    DispatchQueue.main.asyncAfter(deadline: .now() + 1){
                        data.append(
                            contentsOf: (11...20).map{$0}
                        )
                    }
                }
            }
        }
        .itemSize(.init(width: 70, height: 70))
    }
}

Don’t worry about the height

struct ExampleView: View {
    @State var data: [Int] = (0...10).map{$0}
    var body: some View {
        CollectionView(self.data) { index, element in
            
            let height: CGFloat? = {
                if index % 2 == 0 {
                    return 150
                } else if index % 3 == 0 {
                    return 200
                } else {
                    return nil
                }
            }()
            
            VStack{
                Text("Index: \(index)")
                Text("Element: \(element)")
            }
            .fillSpace(.horizontal)
            .frame(height: height)
            .background(Color.red)
            
        }
        .gridColumns(1)
    }
}

struct ExampleView: View {
    @State var data: [Int] = (0...10).map{$0}
    var body: some View {
        CollectionView(self.data) { index, element in
            
            let padding: CGFloat = {
                if index % 2 == 0 {
                    return 20
                } else if index % 3 == 0 {
                    return 40
                } else {
                    return 60
                }
            }()
            
            VStack{
                Text("Index: \(index)")
                    .padding(.bottom, padding)
                Text("Element: \(element)")
            }
            .fillSpace(.horizontal)
            .padding(.vertical, 5)
            .background(Color.red)
            
        }
        .gridColumns(1)
    }
}

Update your data

struct ExampleView: View {
    @State var data: [Int] = (0...10).map{$0}
    var body: some View {
        CollectionView(self.data) { index, element in
            VStack{
                Text("Index: \(index)")
                Text("Element: \(element)")
            }
            .fillSpace(.horizontal)
            .background(Color.red)
            
        }
        .gridColumns(1)
        .refreshAction { completed in
            self.data = []
            DispatchQueue.main.asyncAfter(deadline: .now() + 1){
                self.data = (20...33).map{$0}
                completed()
            }
        }
        .ignoresSafeArea()
    }
}

Features of use

To achieve better performance, the package does not use constraints and the size is calculated based on the contents of the view. The data passes through the buffer and after calculations, the dimensions are saved for later reuse. Disable size caching if your content may change size after updating (then you can turn it back on)

In addition to sizes, representations are also not recalculated and are saved for further reuse. Do not update the data source at once, otherwise you will end up with old views. Use asynchronous operations for updates.

GitHub

View Github