ハッシュタグ風UIをAndroid / iOSアプリで実装

はじめまして。去年11月に入社したエンジニアの佐藤です。

現在は主に Android アプリエンジニアとして開発に携わっています。 自分のAndroidアプリに関しての未熟さ(Androidアプリ開発歴1.5年ほど)を痛感しながら、他のメンバーの皆さま方から学ぶことが多い日々です。

他の皆さんが書くような難しい内容は、わたしにはまだ書けないので、今回は最近担当した↓の実装について書こうと思います。

f:id:hikaru-sato:20190121164221p:plain
ハッシュタグ風UI

ユーザー様検索結果で各ユーザー様の「スキル/利用できるツールを表示する」というもので、これを赤枠のように表示する、という、よくあるハッシュタグ風な表示の実装です。

CSSですと、 display: flex; などで表現するようなところだと思います。

Android アプリ

flexbox-layout というライブラリを使ったら簡単にできました。安心のGoogle 製ライブラリです。

github.com

サンプルアプリを作る形式で説明していこうと思います。

今回作ったサンプルのソースはこちら。

GitHub - HikaruSato/flexboxExample: https://github.com/google/flexbox-layout を使った ハッシュタグ風UIのExample。Android版

↓サンプルの作成手順

まず、Android Studioで、適当に空のActivityを持つプロジェクトを作成します。

f:id:hikaru-sato:20190121170511p:plain

build.gredle

dependencies に、com.google.android:flexbox を追加するのですが、

flexbox-layout の README にあるように、

  • AndroidX を利用しているプロジェクトの場合
implementation 'com.google.android:flexbox:1.1.0'
  • AndroidX を利用していないプロジェクトの場合
implementation 'com.google.android:flexbox:1.0.0'

dependencies に追加します。

drawable/bg_round_rectangle.xml

ハッシュタグ表示の背景用に drawable resource を追加しておきます。

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <solid android:color="#B38ec31f" />

    <corners
        android:radius="8dp" />
</shape>

layout/activity_main.xml

MainActivityに固定のハッシュタグを表示する場合(実際はそういうケースは少ないと思いますが)は下記のようなlayoutを書きます(data binding 利用を前提にしています🙇‍♂️)。

<?xml version="1.0" encoding="utf-8" ?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.constraint.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            tools:context=".MainActivity">

            <com.google.android.flexbox.FlexboxLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="12dp"
                app:alignContent="flex_start"
                app:alignItems="flex_start"
                app:flexWrap="wrap"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent">

                <TextView
                    android:id="@+id/txt1"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_margin="4dp"
                    android:background="@drawable/bg_round_rectangle"
                    android:padding="4dp"
                    android:text="こんにゃく"
                    android:textColor="#FFFFFF" />


                <TextView
                    android:id="@+id/txt2"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_margin="4dp"
                    android:background="@drawable/bg_round_rectangle"
                    android:padding="4dp"
                    android:text="エリスリトール"
                    android:textColor="#FFFFFF" />

                <TextView
                    android:id="@+id/txt3"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_margin="4dp"
                    android:background="@drawable/bg_round_rectangle"
                    android:padding="4dp"
                    android:text="大豆"
                    android:textColor="#FFFFFF" />

                <TextView
                    android:id="@+id/txt4"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_margin="4dp"
                    android:background="@drawable/bg_round_rectangle"
                    android:padding="4dp"
                    android:text="大豆製品(豆腐, 湯葉, 油揚げ, 納豆など)"
                    android:textColor="#FFFFFF" />

                <TextView
                    android:id="@+id/txt5"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_margin="4dp"
                    android:background="@drawable/bg_round_rectangle"
                    android:padding="4dp"
                    android:text="枝豆"
                    android:textColor="#FFFFFF" />


                <TextView
                    android:id="@+id/txt6"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_margin="4dp"
                    android:background="@drawable/bg_round_rectangle"
                    android:padding="4dp"
                    android:text="無調整豆乳"
                    android:textColor="#FFFFFF" />

                <TextView
                    android:id="@+id/txt7"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_margin="4dp"
                    android:background="@drawable/bg_round_rectangle"
                    android:padding="4dp"
                    android:text="アーモンド"
                    android:textColor="#FFFFFF" />

            </com.google.android.flexbox.FlexboxLayout>

        </android.support.constraint.ConstraintLayout>
    </ScrollView>
</layout>

これだけでAndroid Studioで下記のような Preview が表示されていると思います。

f:id:hikaru-sato:20190121174144p:plain

FlexboxLayout の属性で利用したもの(1つだけですが)について説明すると、

app:flexWrap

FlexboxLayout 内のView を 1行(nowrap default)にするか、 複数行(wrap)にするか、 複数行で逆順にするか(wrap_reverse)を設定します。 今回の例では複数行(wrap)にしています。

それ以外を設定した場合、下記のようになります。

app:flexWrap="nowrap"

1行に収める。

f:id:hikaru-sato:20190121175804p:plain
nowrap

app:flexWrap="wrap_reverse"

行の順番が逆になる。

f:id:hikaru-sato:20190121175903p:plain
wrap_reverse

FlexboxLayout内を コードで 動的に追加する場合

下記のように書けます。

layout/activity_main.xml

<?xml version="1.0" encoding="utf-8" ?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.constraint.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            tools:context=".MainActivity">

            <com.google.android.flexbox.FlexboxLayout
                android:id="@+id/flex"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="12dp"
                app:flexWrap="wrap"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        </android.support.constraint.ConstraintLayout>
    </ScrollView>
</layout>

layout/item_tag.xml

ハッシュタグ用のレイアウトを追加しておきます。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <LinearLayout android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/txt"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="4dp"
            android:background="@drawable/bg_round_rectangle"
            android:padding="4dp"
            android:textColor="#FFFFFF"
            tools:text="アーモンド" />

    </LinearLayout>

</layout>

MainActivity.kt

package com.example.flexbox.flexboxexample

import com.example.flexbox.flexboxexample.databinding.ActivityMainBinding
import android.databinding.DataBindingUtil
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.view.LayoutInflater
import com.example.flexbox.flexboxexample.databinding.ItemTagBinding

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

        // 糖質の少ない食品
        val keywords = arrayListOf("こんにゃく", "エリスリトール", "大豆製品(豆腐, 湯葉, 油揚げ, 納豆など)", "枝豆", "無調整豆乳", "アーモンド", "杏仁", "カシューナッツ", "くるみ", "けし", "ごま", "ピスタチオ", "ピーナッツ", "マカダミアナッツ", "オクラ", "かぶ", "カリフラワー", "キャベツ", "キュウリ", "小松菜", "ごぼう", "大根", "タケノコ", "玉ねぎ", "チンゲン菜", "トマト", "なす", "にら", "にんじん", "にんにく", "ねぎ", "白菜", "パプリカ", "ピーマン", "ふき", "ブロッコリー", "ホウレン草", "もやし", "レタス", "アボカド", "きのこ類", "魚", "肉", "卵", "チーズ", "生クリーム", "バター", "牛乳", "ウイスキー", "ウォッカ", "焼酎", "ジン", "ラム", "ワイン(甘口は要注意)", "コーヒー", "紅茶", "日本茶", "ウーロン茶", "プーアル茶", "ジャスミン茶", "コーラゼロ", "こしょう", "塩", "しょうゆ", "酢", "白みそ以外のみそ", "マヨネーズ","こんにゃく", "エリスリトール", "大豆", "大豆製品(豆腐", "湯葉", "油揚げ", "納豆など)", "枝豆", "無調整豆乳", "アーモンド", "杏仁", "カシューナッツ", "くるみ", "けし", "ごま", "ピスタチオ", "ピーナッツ", "マカダミアナッツ", "オクラ", "かぶ", "カリフラワー", "キャベツ", "キュウリ", "小松菜", "ごぼう", "大根", "タケノコ", "玉ねぎ", "チンゲン菜", "トマト", "なす", "にら", "にんじん", "にんにく", "ねぎ", "白菜", "パプリカ", "ピーマン", "ふき", "ブロッコリー", "ホウレン草", "もやし", "レタス", "アボカド", "きのこ類", "魚", "肉", "卵", "チーズ", "生クリーム", "バター", "牛乳", "ウイスキー", "ウォッカ", "焼酎", "ジン", "ラム", "ワイン(甘口は要注意)", "コーヒー", "紅茶", "日本茶", "ウーロン茶", "プーアル茶", "ジャスミン茶", "コーラゼロ", "こしょう", "塩", "しょうゆ", "酢", "白みそ以外のみそ", "マヨネーズ","こんにゃく", "エリスリトール", "大豆", "大豆製品(豆腐", "湯葉", "油揚げ", "納豆など)", "枝豆", "無調整豆乳", "アーモンド", "杏仁", "カシューナッツ", "くるみ", "けし", "ごま", "ピスタチオ", "ピーナッツ", "マカダミアナッツ")

        keywords.forEach {
            val itemTagBinding = DataBindingUtil.inflate<ItemTagBinding>(LayoutInflater.from(this), R.layout.item_tag, binding.flex, true)
            itemTagBinding.txt.text = it
        }

    }
}

実動作イメージ

f:id:hikaru-sato:20190121182515p:plain
実動作イメージ(Android)

iOS アプリ

※ココナラアプリのiOS版のハッシュタグ表示はわたしが担当したわけではないです。すいません🙇‍♂️

iOSでハッシュタグ風UIを実現するのに手頃なオープンソースライブラリは、ありそうではありましたが、普通に標準ライブラリのUICollectionView関連の実装で書けそうでした。

今回作ったサンプルのソースはこちら。

GitHub - HikaruSato/KeywordCollectionViewFlowLayout: ハッシュタグ風UIのExample。iOS版

↓サンプルの作成手順

まず適当に空のプロジェクトを Xcodeで作成します。

f:id:hikaru-sato:20190121182840p:plain

Main.storyboard

ストーリーボードのViewController の下に CollectionViewを追加して、cellにLabelを追加します。

f:id:hikaru-sato:20190121183147p:plain

また、UICollectionViewFlowLayouのClass欄にはカスタマイズするため、KeywordCollectionViewFlowLayout(下記で定義するクラス)を記載しておきます。

f:id:hikaru-sato:20190121184207p:plain

ViewController.swift

import UIKit

class ViewController: UICollectionViewController{

    fileprivate let cellIdentifier = "cell"
    
    let labelOuterMargin: CGFloat = 5
    let labelInsetMargin: CGFloat = 10
    var cellAmimationStatusDic: [Int : Bool] = [:]
    
    // 糖質の少ない食品
    let keywords = ["こんにゃく", "エリスリトール", "大豆製品(豆腐, 湯葉, 油揚げ, 納豆など)", "枝豆", "無調整豆乳", "アーモンド", "杏仁", "カシューナッツ", "くるみ", "けし", "ごま", "ピスタチオ", "ピーナッツ", "マカダミアナッツ", "オクラ", "かぶ", "カリフラワー", "キャベツ", "キュウリ", "小松菜", "ごぼう", "大根", "タケノコ", "玉ねぎ", "チンゲン菜", "トマト", "なす", "にら", "にんじん", "にんにく", "ねぎ", "白菜", "パプリカ", "ピーマン", "ふき", "ブロッコリー", "ホウレン草", "もやし", "レタス", "アボカド", "きのこ類", "魚", "肉", "卵", "チーズ", "生クリーム", "バター", "牛乳", "ウイスキー", "ウォッカ", "焼酎", "ジン", "ラム", "ワイン(甘口は要注意)", "コーヒー", "紅茶", "日本茶", "ウーロン茶", "プーアル茶", "ジャスミン茶", "コーラゼロ", "こしょう", "塩", "しょうゆ", "酢", "白みそ以外のみそ", "マヨネーズ","こんにゃく", "エリスリトール", "大豆", "大豆製品(豆腐", "湯葉", "油揚げ", "納豆など)", "枝豆", "無調整豆乳", "アーモンド", "杏仁", "カシューナッツ", "くるみ", "けし", "ごま", "ピスタチオ", "ピーナッツ", "マカダミアナッツ", "オクラ", "かぶ", "カリフラワー", "キャベツ", "キュウリ", "小松菜", "ごぼう", "大根", "タケノコ", "玉ねぎ", "チンゲン菜", "トマト", "なす", "にら", "にんじん", "にんにく", "ねぎ", "白菜", "パプリカ", "ピーマン", "ふき", "ブロッコリー", "ホウレン草", "もやし", "レタス", "アボカド", "きのこ類", "魚", "肉", "卵", "チーズ", "生クリーム", "バター", "牛乳", "ウイスキー", "ウォッカ", "焼酎", "ジン", "ラム", "ワイン(甘口は要注意)", "コーヒー", "紅茶", "日本茶", "ウーロン茶", "プーアル茶", "ジャスミン茶", "コーラゼロ", "こしょう", "塩", "しょうゆ", "酢", "白みそ以外のみそ", "マヨネーズ","こんにゃく", "エリスリトール", "大豆", "大豆製品(豆腐", "湯葉", "油揚げ", "納豆など)", "枝豆", "無調整豆乳", "アーモンド", "杏仁", "カシューナッツ", "くるみ", "けし", "ごま", "ピスタチオ", "ピーナッツ", "マカダミアナッツ", "オクラ", "かぶ", "カリフラワー", "キャベツ", "キュウリ", "小松菜", "ごぼう", "大根", "タケノコ", "玉ねぎ", "チンゲン菜", "トマト", "なす", "にら", "にんじん", "にんにく", "ねぎ", "白菜", "パプリカ", "ピーマン", "ふき", "ブロッコリー", "ホウレン草", "もやし", "レタス", "アボカド", "きのこ類", "魚", "肉", "卵", "チーズ", "生クリーム", "バター", "牛乳", "ウイスキー", "ウォッカ", "焼酎", "ジン", "ラム", "ワイ<e3><83><b3>(甘口は要注意)", "コーヒー", "紅茶", "日本茶", "ウーロン茶", "プーアル茶", "ジャスミン茶", "コーラゼロ", "こしょう", "塩", "しょうゆ", "酢", "白みそ以外のみそ", "マヨネーズ","こんにゃく", "エリスリトール", "大豆", "大豆製品(豆腐", "湯葉", "油揚げ", "納豆など)", "枝豆", "無調整豆乳", "アーモンド", "杏仁", "カシューナッツ", "くるみ", "けし", "ごま", "ピスタチオ", "ピーナッツ", "マカダミアナッツ", "オクラ", "かぶ", "カリフラワー", "キャベツ", "キュウリ", "小松菜", "ごぼう", "大根", "タケノコ", "玉ねぎ", "チンゲン菜", "トマト", "なす", "にら", "にんじん", "にんにく", "ねぎ", "白菜", "パプリカ", "ピーマン", "ふき", "ブロッコリー", "ホウレン草", "もやし", "レタス", "アボカド", "きのこ類", "魚", "肉", "卵", "チーズ", "生クリーム", "バター", "牛乳", "ウイスキー", "ウォッカ", "焼酎", "ジン", "ラム", "ワイン(甘口は要注意)", "コーヒー", "紅茶", "日本茶", "ウーロン茶", "プーアル茶", "ジャスミン茶", "コーラゼロ", "こしょう", "塩", "しょうゆ", "酢", "白みそ以外のみそ", "マヨネーズ","こんにゃく", "エリスリトール", "大豆", "大豆製品(豆腐", "湯葉", "油揚げ", "納豆など)", "枝豆", "無調整豆乳", "アーモンド", "杏仁", "カシューナッツ", "くるみ", "けし", "ごま", "ピスタチオ", "ピーナッツ", "マカダミアナッツ", "オクラ", "かぶ", "カリフラワー", "キャベツ", "キュウリ", "小松菜", "ごぼう", "大根", "タケノコ", "玉ねぎ", "チンゲン菜", "トマト", "なす", "にら", "にんじん", "にんにく", "ねぎ", "白菜", "パプリカ", "ピーマン", "ふき", "ブロッコリー", "ホウレン草", "もやし", "レタス", "アボカド", "きのこ類", "魚", "肉", "卵", "チーズ", "生クリーム", "バター", "牛乳", "ウイスキー", "ウォッカ", "焼酎", "ジン", "ラム", "ワイン(甘口は要注意)", "コーヒー", "紅茶", "日本茶", "ウーロン茶", "プーアル茶", "ジャスミン茶", "コーラゼロ", "こしょう", "塩", "しょうゆ", "酢", "白みそ以外のみそ", "マヨネーズ"]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        if let  layout = self.collectionView?.collectionViewLayout as? KeywordCollectionViewFlowLayout {
            layout.sectionInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
            layout.minimumLineSpacing = 8
            layout.minimumInteritemSpacing = 8
            layout.delegate = self
        }
    }

    override var prefersStatusBarHidden : Bool {
        return true
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return keywords.count
    }
    
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
        let mainLabel = cell.viewWithTag(1) as! UILabel
        mainLabel.text = keywords[indexPath.row]
        mainLabel.sizeToFit()
        mainLabel.frame.size = CGSize(width: getLabelWidth(label: mainLabel), height: 40)
        mainLabel.layer.cornerRadius = 5
        mainLabel.layer.masksToBounds = true
        
        // 最初の描画時にアニメーションしてみる
        if !cellAmimationStatusDic.contains(where: { (key, value) -> Bool in
            key == indexPath.row
        }) {
            cellAmimationStatusDic[indexPath.row] = true
            let orgFrame = cell.frame
            let random = arc4random() % 10
            let delay = TimeInterval(random) / 10
            cell.frame = CGRect(x: cell.frame.origin.x, y: cell.frame.origin.y + UIScreen.main.bounds.height, width: cell.frame.width, height: cell.frame.height)
            UIView.animate(withDuration: 1.3, delay: delay, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: UIView.AnimationOptions.curveEaseInOut, animations: {
                cell.frame = orgFrame
            }, completion: nil)
        }
        
        return cell
    }
    
    fileprivate func getLabelWidth(label:UILabel) -> CGFloat {
        return label.frame.width + labelInsetMargin * 2
    }
}

extension ViewController:KeywordCollectionViewFlowLayoutDelegate {
    func collectionView(collectionView:UICollectionView, widthAtIndexPath indexPath:IndexPath) -> CGFloat {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 15)
        label.text = keywords[indexPath.row]
        label.sizeToFit()
        return getLabelWidth(label: label) + labelOuterMargin * 2
    }
}

KeywordCollectionViewFlowLayout.swift

import UIKit

protocol KeywordCollectionViewFlowLayoutDelegate {
    func collectionView(collectionView:UICollectionView, widthAtIndexPath indexPath:IndexPath) -> CGFloat
}

open class KeywordCollectionViewFlowLayout: UICollectionViewFlowLayout {
    
    var delegate: KeywordCollectionViewFlowLayoutDelegate!
    fileprivate var sectionCellRects = [[CGRect]]()
    fileprivate var contentSize = CGSize.zero
    
    override open func prepare() {
        super.prepare()
        
        if (!sectionCellRects.isEmpty) {
            return
        }
        
        guard let collectionView = self.collectionView else {
            return
        }

        let cellHeight: CGFloat = itemSize.height
        let maxWidth = collectionView.bounds.width - collectionView.contentInset.left - collectionView.contentInset.right
        contentSize = CGSize(width: 0, height: 0)
        var xOffset: CGFloat = super.sectionInset.left
        var yOffset: CGFloat = super.sectionInset.top
        for section in (0..<collectionView.numberOfSections) {
            var cells = [CGRect]()
            let numberOfCellsInSection = collectionView.numberOfItems(inSection: section)
            
            var height = contentSize.height
            
            for i in (0..<numberOfCellsInSection) {
                let cellwidth = delegate.collectionView(collectionView: collectionView, widthAtIndexPath: IndexPath(row: i, section: section))
                var x = xOffset
                if x + cellwidth + super.minimumInteritemSpacing > maxWidth {
                    //New Line
                    x = super.sectionInset.left
                    yOffset += cellHeight + super.minimumLineSpacing
                    xOffset = x + cellwidth + minimumInteritemSpacing
                } else {
                    xOffset += cellwidth + super.minimumInteritemSpacing
                }
                let y = yOffset
                
                let cellRect = CGRect(x: x, y: y, width: cellwidth, height: cellHeight)
                cells.append(cellRect)
                if (height < cellRect.origin.y + cellRect.height) {
                    height = cellRect.origin.y + cellRect.height
                }
            }
            height += cellHeight + super.minimumLineSpacing
            contentSize = CGSize(width: maxWidth, height: height)
            sectionCellRects.append(cells)
        }
    }
    
    override open func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        super.layoutAttributesForElements(in: rect)
        var layoutAttributes = [UICollectionViewLayoutAttributes]()
        
        if let collectionView = self.collectionView {
            for  i in 0  ..< collectionView.numberOfSections {
                let numberOfCellsInSection = collectionView.numberOfItems(inSection: i)
                for j in 0 ..< numberOfCellsInSection {
                    let indexPath = IndexPath(row:j, section:i)
                    if let attributes = layoutAttributesForItem(at: indexPath) {
                        if (rect.intersects(attributes.frame)) {
                            layoutAttributes.append(attributes)
                        }
                    }
                }
            }
        }
        return layoutAttributes
    }
    
    override open func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = super.layoutAttributesForItem(at: indexPath)
        attributes?.frame = sectionCellRects[indexPath.section][indexPath.row]
        return attributes
    }
    
    override open var collectionViewContentSize : CGSize {
        return contentSize
    }
}

実動作イメージ

f:id:hikaru-sato:20190121185256g:plain
実動作イメージ(iOS)

まとめ

AndroidアプリとiOSアプリは似たようなイメージですが、実装は結構違ったりすると思います(何を実装するかにもよりますが)。

ちなみにココナラでは一緒にココナラを改善してくれる仲間も募集しております。

www.wantedly.com