SwiftUIで正方形のアイテム(iOSフォトライブラリなど)のグリッドを作成するにはどうすればよいですか?
私はこのアプローチを試みましたが、うまくいきません:
var body: some View {
List(cellModels) { _ in
Color.orange.frame(width: 100, height: 100)
}
}
リストにはまだ UITableView スタイルがあります:
SwiftUIで正方形のアイテム(iOSフォトライブラリなど)のグリッドを作成するにはどうすればよいですか?
私はこのアプローチを試みましたが、うまくいきません:
var body: some View {
List(cellModels) { _ in
Color.orange.frame(width: 100, height: 100)
}
}
リストにはまだ UITableView スタイルがあります:
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)
}
}
}
デフォルトのレイアウト構成をカスタマイズすることもできます:
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)
}
}
}
GitHub リポジトリ内のデモ GIF とテスト アプリを参照してください。
SwiftUI で使用するフル機能の CollectionView を提供する迅速なパッケージを開発しました。
ここで見つけてください: https://github.com/apptekstudios/ASCollectionView
使いやすいように設計されていますが、より複雑なレイアウト用に新しい UICollectionViewCompositionalLayout をフルに活用することもできます。セルの自動サイズ変更をサポートしています。
グリッド ビューを実現するには、次のように使用できます。
import SwiftUI
import ASCollectionView
struct ExampleView: View {
@State var dataExample = (0 ..< 21).map { $0 }
var body: some View
{
ASCollectionView(data: dataExample, dataID: \.self) { item, _ in
Color.blue
.overlay(Text("\(item)"))
}
.layout {
.grid(layoutMode: .adaptive(withMinItemSize: 100),
itemSpacing: 5,
lineSpacing: 5,
itemSize: .absolute(50))
}
}
}
はるかに複雑なレイアウトの例については、デモ プロジェクトを参照してください。
私はこの問題に自分で取り組んでおり、@ 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
クラスにより、バインド可能なスナップショットにアクセスし、必要に応じて UI を更新できるようになります。このクラスは、コレクション ビューを適切に更新するために不可欠です。モデルDataSection
とモデルは、 Avery VineDataObject
が提供するものと同じ構造です。必要な場合は、そちらを参照してください。
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 プレビュー ウィンドウで実行されています) だけです。
更新: この回答は iOS 13 に関連しています。iOS 14 の場合、LazyGrids + もっと多くのものがあるため、この回答に従うことは役に立ちません。
UIKit を使用せずに CollectionView を作成するには、まず配列拡張が必要です。配列拡張は、TableView を作成したい配列をチャンクするのに役立ちます。以下は、拡張機能のコード、+ 3 つの例です。この拡張機能がどのように機能するかをもう少し理解するには、拡張機能をコピーしたこのサイトをご覧ください: https://www.hackingwithswift.com/example-code/language/how-to-split-チャンクへの配列
extension Array {
func chunked(into size: Int) -> [[Element]] {
return stride(from: 0, to: count, by: size).map {
Array(self[$0 ..< Swift.min($0 + size, count)])
}
}
}
let exampleArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
print(exampleArray.chunked(into: 2)) // prints [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11, 12]]
print(exampleArray.chunked(into: 3)) // prints [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
print(exampleArray.chunked(into: 5)) // prints [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12]]
次に、SwiftUI ビューを作成します。
struct TestView: View {
let arrayOfInterest = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18].chunked(into: 4)
// = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16], [17, 18]]
var body: some View {
return VStack {
ScrollView {
VStack(spacing: 16) {
ForEach(self.arrayOfInterest.indices, id:\.self) { idx in
HStack {
ForEach(self.arrayOfInterest[idx].indices, id:\.self) { index in
HStack {
Spacer()
Text("\(self.arrayOfInterest[idx][index])")
.font(.system(size: 50))
.padding(4)
.background(Color.blue)
.cornerRadius(8)
Spacer()
}
}
}
}
}
}
}
}
}
struct TestView_Preview : PreviewProvider {
static var previews: some View {
TestView()
}
}
説明:
まず、必要な列数を明確にし、その数をチャンク拡張機能に入れる必要があります。私の例では、ビューに表示したい 1 から 18 までの数値の配列 (arrayOfInterest) があり、ビューに 4 つの列を持たせることにしたので、それを 4 にチャンクしました (したがって 4 は数値です)。私たちのコラムの)。
CollectionView を作成するには、CollectionView が項目の LIST であるため、簡単にスクロールできるようにするためにリストに含める必要があります (いいえ、そうしないでください! 代わりに ScrollView を使用してください。奇妙な動作を見てきました)。これらの 2 つの foreach はリストにあります)。ScrollView の後に 2 つの ForEach があります。1 つ目は必要な数の行をループできるようにし、2 つ目は列を作成するのに役立ちます。
コードを完全に説明していないことは承知していますが、テーブル ビューを簡単にするために共有する価値があると確信しています。 この画像は、私が作成している実際のアプリの初期の例であり、 CollectionView にほかならないので、このアプローチがうまく機能していることを確認できます。
質問: 配列を持っていて、迅速に foreach のインデックスを作ろうとするポイントは何ですか?
それは簡単です!実行時に値/値の数を定義する配列がある場合、たとえば、Web API から数値を取得していて、その API が配列内の数値を教えてくれる場合は、いくつかのアプローチを使用する必要があります。このようにして、swift に foreach のインデックスを処理させます。
更新:
詳細情報、これらを読むことはオプションです。
LIST VS SCROLLVIEW : ご存じない方もいらっしゃるかもしれませんが、リストはスクロール ビューとは少し異なります。スクロールビューを作成すると、常にスクロールビュー全体が計算され、表示されます。リストを使用する場合、swiftは現在のビューを表示するために必要なリストのコンポーネントのいくつかのみを自動的に計算し、リストの一番下までスクロールすると、現在の古い値のみが置き換えられます。画面の下部にあるものの新しい値で、スクロールアウトします。そのため、一般的にリストは常に軽量であり、重いビューで作業している場合ははるかに高速になる可能性があります。これは、最初にすべてのビューを計算するのではなく、必要なものだけを計算するのに対し、ScrollView は計算しないためです。
リストの代わりにスクロールビューを使用する必要があると言ったのはなぜですか? 前に言ったように、おそらく気に入らないリストとのやり取りがいくつかあります。たとえば、リストを作成する場合、すべての行がタップ可能であることは問題ありませんが、行全体のみがタップ可能であることは問題ではありません。つまり、行の左側にタップ アクションを設定し、右側に別のタップ アクションを設定することはできません。これは、 List() の奇妙な相互作用の 1 つにすぎません。これには、私が持っていない知識が必要です。または、xcode-ios の大きな問題であるか、意図したとおりに問題ない可能性があります。私が思うに、これはアップルの問題であり、せいぜい次の WWDC までに修正されることを願っています。(更新:もちろん、iOS14-SwiftUI用のLazyGridsなどのすべてのものの導入で修正されました)
この問題を解決する方法はありますか? 私の知る限り、唯一の方法は UIKit を使用することです。私は SwiftUI で多くの方法を試しましたが、ActionSheet と ContextMenu を使用してリストをタップしたときのオプションの面で改善できることがわかりましたが、目的の最適な機能を得ることができませんでしたSwiftUI リスト。私の視点から見ると、SwiftUI 開発者は今のところ待つしかありません。