UICollectionView и SwiftUI?

Как создать сетку из квадратных элементов (например, как в iOS Photo Library) с SwiftUI?

Я попробовал этот подход, но он не работает:

var body: some View {
    List(cellModels) { _ in
        Color.orange.frame(width: 100, height: 100)
    }
}

Список по-прежнему имеет стиль UITableView:

enter image description here

Ответ 1

Одно из возможных решений - обернуть ваш UICollectionView в UIViewRepresentable. См. Руководство по комбинированию и созданию представлений SwiftUI, где они обертывают MKMapView в качестве примера.

К настоящему UICollectionView в SwiftUI нет эквивалента UICollectionView, и пока у него нет планов на это. Смотрите обсуждение под этим твитом.

Чтобы получить более подробную информацию, посмотрите видеоролик "Интеграция SwiftUI WWDC" (~ 8: 08).

Ответ 2

QGrid - это небольшая библиотека, которую я создал, которая использует тот же подход, что и представление SwiftUI List, вычисляя его ячейки по требованию из базовой коллекции идентифицированных данных:

В простейшем виде QGrid можно использовать только с этой 1 строкой кода в теле вашего View, при условии, что у вас уже есть пользовательский вид ячейки:

struct PeopleView: View {
  var body: some View {
    QGrid(Storage.people, columns: 3) { GridCell(person: $0) }
  }
}   

struct GridCell: View {
  var person: Person
  var body: some View {
    VStack() {
      Image(person.imageName).resizable().scaledToFit()
      Text(person.firstName).font(.headline).color(.white)
      Text(person.lastName).font(.headline).color(.white)
    }
  }
}

enter image description here


Вы также можете настроить конфигурацию макета по умолчанию:

struct PeopleView: View {
  var body: some View {
    QGrid(Storage.people,
          columns: 3,
          columnsInLandscape: 4,
          vSpacing: 50,
          hSpacing: 20,
          vPadding: 100,
          hPadding: 20) { person in
            GridCell(person: person)
    }
  }
} 

Пожалуйста, ознакомьтесь с демонстрационным GIF и тестовым приложением в репозитории GitHub:

https://github.com/Q-Mobile/QGrid

Ответ 3

Размышляя в SwiftUI, есть простой способ:

struct MyGridView : View {
var body: some View {
    List() {
        ForEach(0..<8) { _ in
            HStack {
                ForEach(0..<3) { _ in
                    Image("orange_color")
                        .resizable()
                        .scaledToFit()
                }
            }
        }
    }
}

}

SwiftUI достаточно, если вы хотите, вам нужно забыть, например, UIColectionView иногда..

enter image description here

Ответ 4

Я написал небольшой компонент с именем ridGridStack, который создает сетку, которая подстраивается под доступную ширину. Даже когда это меняется динамически, как при повороте iPad.

https://github.com/pietropizzi/GridStack

HStack этой реализации аналогичны тем, что другие ответили здесь (поэтому HStack внутри VStack) с той разницей, что она VStack ширину в зависимости от доступной ширины и конфигурации, которую вы передаете.

  • С minCellWidth вы определяете наименьшую ширину, которую должен иметь ваш элемент в сетке.
  • С spacing вы определяете пространство между элементами в сетке.

например

GridStack(
    minCellWidth: 320,
    spacing: 15,
    numItems: yourItems.count
) { index, cellWidth in
    YourItemView(item: yourItems[index]).frame(width: cellWidth)
}

Ответ 5

XCode 11.0

Посмотрев некоторое время, я решил, что хочу получить все удобство и производительность от UICollectionView. Поэтому я реализовал протокол UIViewRepresentable.

Этот пример не реализует DataSource и имеет фиктивное поле data: [Int] в представлении коллекции. Вы бы использовали @Bindable var data: [YourData] на AlbumGridView для автоматической перезагрузки вашего вида при изменении данных.

AlbumGridView можно затем использовать как любой другой вид внутри SwiftUI.

Код

class AlbumPrivateCell: UICollectionViewCell {
    private static let reuseId = "AlbumPrivateCell"

    static func registerWithCollectionView(collectionView: UICollectionView) {
        collectionView.register(AlbumPrivateCell.self, forCellWithReuseIdentifier: reuseId)
    }

    static func getReusedCellFrom(collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> AlbumPrivateCell{
        return collectionView.dequeueReusableCell(withReuseIdentifier: reuseId, for: indexPath) as! AlbumPrivateCell
    }

    var albumView: UILabel = {
        let image = UILabel()
        image.backgroundColor = .black
        return image
    }()

    override init(frame: CGRect) {
        super.init(frame: .zero)
        contentView.addSubview(self.albumView)

        albumView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
        albumView.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true
        albumView.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true
        albumView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
    }

    required init?(coder: NSCoder) {
        fatalError("init?(coder: NSCoder) has not been implemented")
    }
}

struct AlbumGridView: UIViewRepresentable {
    var data = [1,2,3,4,5,6,7,8,9]

    func makeUIView(context: Context) -> UICollectionView {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .vertical

        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.backgroundColor = .blue
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.dataSource = context.coordinator
        collectionView.delegate = context.coordinator

        AlbumPrivateCell.registerWithCollectionView(collectionView: collectionView)
        return collectionView

    }

    func updateUIView(_ uiView: UICollectionView, context: Context) {
        //
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UICollectionViewDelegate {
        let parent: AlbumGridView

        init(_ albumGridView: AlbumGridView) {
            self.parent = albumGridView
        }

        // MARK: UICollectionViewDataSource
        func numberOfSections(in collectionView: UICollectionView) -> Int {
            return 1
        }

        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return self.parent.data.count
        }

        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let albumCell = AlbumPrivateCell.getReusedCellFrom(collectionView: collectionView, cellForItemAt: indexPath)
            albumCell.backgroundColor = .red

            return albumCell
        }

        // MARK: UICollectionViewDelegateFlowLayout
        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
            let width = collectionView.frame.width / 3
            return CGSize(width: width, height: width)
        }

        // MARK: UICollectionViewDelegate
    }
}

Скриншот

AlbumGridView Preview

Ответ 6

Попробуйте использовать VStack и HStack

var body: some View {
    GeometryReader { geometry in
        VStack {
            ForEach(1...3) {_ in
                HStack {
                    Color.orange.frame(width: 100, height: 100)
                    Color.orange.frame(width: 100, height: 100)
                    Color.orange.frame(width: 100, height: 100)
                }.frame(width: geometry.size.width, height: 100)
            }
        }
    }
}

Вы можете обернуть в ScrollView, если вы хотите прокрутку

Ответ 7

Пример оформления заказа на основе ZStack здесь

Grid(0...100) { _ in
    Rectangle()
        .foregroundColor(.blue)
}

enter image description here

Ответ 8

Основываясь на ответе Уилла, я обернул все это в SwiftUI ScrollView. Таким образом, вы можете добиться горизонтальной (в данном случае) или вертикальной прокрутки.

Он также использует GeometryReader, поэтому его можно рассчитать по размеру экрана.

GeometryReader{ geometry in
 .....
 Rectangle()
    .fill(Color.blue)
    .frame(width: geometry.size.width/6, height: geometry.size.width/6, alignment: .center)
 }

Вот рабочий пример:

import SwiftUI

struct MaterialView: View {

  var body: some View {

    GeometryReader{ geometry in

      ScrollView(Axis.Set.horizontal, showsIndicators: true) {
        ForEach(0..<2) { _ in
          HStack {
            ForEach(0..<30) { index in
              ZStack{
                Rectangle()
                  .fill(Color.blue)
                  .frame(width: geometry.size.width/6, height: geometry.size.width/6, alignment: .center)

                Text("\(index)")
              }
            }
          }.background(Color.red)
        }
      }.background(Color.black)
    }

  }
}

struct MaterialView_Previews: PreviewProvider {
  static var previews: some View {
    MaterialView()
  }
}

enter image description here

Ответ 9

Поскольку я не использую Catalina Beta, я написал здесь свой код, который вы можете запустить на Xcode 11 (Mojave) в качестве игровой площадки, чтобы воспользоваться преимуществами компиляции во время выполнения и Preview

В основном, когда вы ищете подход с использованием сетки, вы должны иметь в виду, что дочерний View SwiftUI получает параметр идеального размера из родительского представления, чтобы они могли автоматически адаптироваться на основе своего собственного содержимого, это поведение может быть переопределено (не путайте с директивой swift Override) путем принудительного просмотра определенного размера с помощью метода .frame(...).

На мой взгляд, это делает поведение View стабильным, так же как и Apple SwiftUI Framework.

import PlaygroundSupport
import SwiftUI

struct ContentView: View {

    var body: some View {

        VStack {
            ForEach(0..<5) { _ in
                HStack(spacing: 0) {
                    ForEach(0..<5) { _ in
                        Button(action: {}) {
                            Text("Ok")
                        }
                        .frame(minWidth: nil, idealWidth: nil, maxWidth: .infinity, minHeight: nil, idealHeight: nil, maxHeight: .infinity, alignment: .center)
                        .border(Color.red)
                    }
                }
            }
        }
    }
}

let contentView = ContentView()
PlaygroundPage.current.liveView = UIHostingController(rootView: contentView)

Ответ 10

Я сам решал эту проблему, и, используя источник, опубликованный выше @Anjali в качестве базы, а также @phillip (работа Avery Vine), я обернул UICollectionView, который функционально... иш? Он будет отображать и обновлять сетку по мере необходимости. Я не пробовал более настраиваемых представлений или каких-либо других вещей, но сейчас, я думаю, это подойдет.

Я прокомментировал мой код ниже, надеюсь, он кому-нибудь пригодится!

Во-первых, обертка.

struct UIKitCollectionView: UIViewRepresentable {
    typealias UIViewType = UICollectionView

    //This is where the magic happens! This binding allows the UI to update.
    @Binding var snapshot: NSDiffableDataSourceSnapshot<DataSection, DataObject>

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: UIViewRepresentableContext<UIKitCollectionView>) -> UICollectionView {

        //Create and configure your layout flow seperately
        let flowLayout = UICollectionViewFlowLayout()
        flowLayout.sectionInsets = UIEdgeInsets(top: 25, left: 0, bottom: 25, right: 0)


        //And create the UICollection View
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)

        //Create your cells seperately, and populate as needed.
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "customCell")

        //And set your datasource - referenced from Avery
        let dataSource = UICollectionViewDiffableDataSource<DataSection, DataObject>(collectionView: collectionView) { (collectionView, indexPath, object) -> UICollectionViewCell? in
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "customCell", for: indexPath)
            //Do cell customization here
            if object.id.uuidString.contains("D") {
                cell.backgroundColor = .red
            } else {
                cell.backgroundColor = .green
            }


            return cell
        }

        context.coordinator.dataSource = dataSource

        populate(load: [DataObject(), DataObject()], dataSource: dataSource)
        return collectionView
    }

    func populate(load: [DataObject], dataSource: UICollectionViewDiffableDataSource<DataSection, DataObject>) {
        //Load the 'empty' state here!
        //Or any default data. You also don't even have to call this function - I just thought it might be useful, and Avery uses it in their example.

        snapshot.appendItems(load)
        dataSource.apply(snapshot, animatingDifferences: true) {
            //Whatever other actions you need to do here.
        }
    }


    func updateUIView(_ uiView: UICollectionView, context: UIViewRepresentableContext<UIKitCollectionView>) {
        let dataSource = context.coordinator.dataSource
        //This is where updates happen - when snapshot is changed, this function is called automatically.

        dataSource?.apply(snapshot, animatingDifferences: true, completion: {
            //Any other things you need to do here.
        })

    }

    class Coordinator: NSObject {
        var parent: UIKitCollectionView
        var dataSource: UICollectionViewDiffableDataSource<DataSection, DataObject>?
        var snapshot = NSDiffableDataSourceSnapshot<DataSection, DataObject>()

        init(_ collectionView: UIKitCollectionView) {
            self.parent = collectionView
        }
    }
}

Теперь класс DataProvider позволит нам получить доступ к этому привязываемому снимку и обновлять пользовательский интерфейс, когда мы этого захотим. Этот класс является необходимым для корректного обновления представления коллекции. Модели DataSection и DataObject имеют ту же структуру, что и модель, предоставленная Avery Vine - так что, если вам это нужно, посмотрите там.

class DataProvider: ObservableObject { //This HAS to be an ObservableObject, or our UpdateUIView function won't fire!
    var data = [DataObject]()

    @Published var snapshot : NSDiffableDataSourceSnapshot<DataSection, DataObject> = {
        //Set all of your sections here, or at least your main section.
        var snap = NSDiffableDataSourceSnapshot<DataSection, DataObject>()
        snap.appendSections([.main, .second])
        return snap
        }() {
        didSet {
            self.data = self.snapshot.itemIdentifiers
            //I set the 'data' to be equal to the snapshot here, in the event I just want a list of the data. Not necessary.
        }
    }

    //Create any snapshot editing functions here! You can also simply call snapshot functions directly, append, delete, but I have this addItem function to prevent an exception crash.
    func addItems(items: [DataObject], to section: DataSection) {
        if snapshot.sectionIdentifiers.contains(section) {
            snapshot.appendItems(items, toSection: section)
        } else {
            snapshot.appendSections([section])
            snapshot.appendItems(items, toSection: section)
        }
    }
}

А теперь, CollectionView, который собирается показать нашу новую коллекцию. Я сделал простой VStack с несколькими кнопками, чтобы вы могли видеть его в действии.

struct CollectionView: View {
    @ObservedObject var dataProvider = DataProvider()

    var body: some View {
        VStack {
            UIKitCollectionView(snapshot: $dataProvider.snapshot)
            Button("Add a box") {
                self.dataProvider.addItems(items: [DataObject(), DataObject()], to: .main)
            }

            Button("Append a Box in Section Two") {
                self.dataProvider.addItems(items: [DataObject(), DataObject()], to: .second)
            }

            Button("Remove all Boxes in Section Two") {
                self.dataProvider.snapshot.deleteSections([.second])
            }
        }
    }
}

struct CollectionView_Previews: PreviewProvider {
    static var previews: some View {
        CollectionView()
    }
}

И только для этих визуальных ссылок (у вас это работает в окне предварительного просмотра Xcode):

UICollectionView meets SwiftUI

Ответ 11

Я думаю, что вы можете использовать прокрутку, как это

struct MovieItemView : View {
    var body: some View {
        VStack {
            Image("sky")
                .resizable()
                .frame(width: 150, height: 200)
            VStack {
                Text("Movie Title")
                    .font(.headline)
                    .fontWeight(.bold)
                Text("Category")
                    .font(.subheadline)
            }
        }
    }
}

struct MoviesView : View {
    var body: some View {
        VStack(alignment: .leading, spacing: 10){
            Text("Now Playing")
                .font(.title)
                .padding(.leading)
            ScrollView {
                HStack(spacing: 10) {
                    MovieItemView()
                    MovieItemView()
                    MovieItemView()
                    MovieItemView()
                    MovieItemView()
                    MovieItemView()
                }
            }
            .padding(.leading, 20)
        }
    }
}