メルカリ・フリルなどのフリマアプリホーム画面の３カラムデザインの作り方

#はじめに
メルカリ/フリルなどのフリマアプリでよく目にするホーム画面の3カラムのデザインを作成する方法を紹介します。
今回はUICollectionViewControllerをSwiftUI上で動かしています。(これでUIKitにホットリロードのCanvasを導入する事が出来ます)

*ストーリーボードは使用していないので、Extension.swiftファイルに制約のルールを設定しています。(ステップ3をご覧ください)

<img width="453" alt="スクリーンショット 0002-07-01 午後5.34.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323176/78cda9a0-777d-43f9-96b8-2b8128359bd6.png">

#開発環境
Swift 5.2.4
Xcode 11.5(Deployment Target 13.0)
ストーリーボードなし

#ステップ１
SceneDelegate.swift内に初期ページ設定をします。初期ページはFrimaContentViewとします

```Swift

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

        // Create the SwiftUI view that provides the window contents.
        let frimaContentView = FrimaContentView()
        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: frimaContentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }
```

#ステップ２： SwiftUIからUICollectionViewControllerを表示する
以下を実装するとUIKitでもホットリロード機能(canvas)が使えるため一々Runしなくてもいいので便利です。

```Swift
import UIKit
import SwiftUI

class FrimaController: UICollectionViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        //背景を白にする
        collectionView.backgroundColor = .white
       
    }
}

struct FrimaIntegratedViewController: UIViewControllerRepresentable {
    
    func makeUIViewController(context: UIViewControllerRepresentableContext<FrimaIntegratedViewController>) -> FrimaController {
        return FrimaController(collectionViewLayout: UICollectionViewFlowLayout())
    }
    
    func updateUIViewController(_ uiViewController: FrimaController, context: Context) {
        
    }
}

struct FrimaContentView: View {
    var body: some View {
        FrimaIntegratedViewController().edgesIgnoringSafeArea(.all)
    }
}

struct FrimaContentView_Previews: PreviewProvider {
    static var previews: some View {
        FrimaContentView()
    }
}

```

#ステップ３： 個々のアイテムセルを作成
セル上にアイテムの名前と値段、いいねボタンを作成します。

```Swift
class FrimaItemCell: UICollectionViewCell {
    
    let priceLabel: UILabel = {
        let label = UILabel()
        label.text = "¥1,000"
        label.textColor = .white
        return label
    }()
    
    let itemNameLabel: UILabel = {
        let label = UILabel()
        label.text = "Tシャツ"
        label.textColor = .white
        return label
    }()
    
    let likeButton: UIButton = {
        let button = UIButton(type: .system)
        //Xcodeにデフォルトである画像を使用 *これはDeplotment Targetが13.0以降で使えます
        button.setImage(UIImage(systemName: "heart")?.withRenderingMode(.alwaysOriginal), for: .normal)
        return button
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)

        backgroundColor = .systemRed
        
        setupView()
    }
    
    fileprivate func setupView() {
        addSubview(priceLabel)
        priceLabel.anchor(top: nil, left: leftAnchor, bottom: bottomAnchor, right: nil, paddingTop: 0, paddingLeft: 5, paddingBottom: 5, paddingRight: 0, height: 0, width: 0)
        
        addSubview(itemNameLabel)
        itemNameLabel.anchor(top: nil, left: priceLabel.leftAnchor, bottom: priceLabel.topAnchor, right: nil, paddingTop: 0, paddingLeft: 0, paddingBottom: 5, paddingRight: 0, height: 0, width: 0)
        
        addSubview(likeButton)
        likeButton.anchor(top: nil, left: nil, bottom: bottomAnchor, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 5, paddingRight: 5, height: 0, width: 0)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
```
制約ルールは別ファイルExtensions.swiftで設定しています。

```Swift

//Extensions.swift
import Foundation
import UIKit

extension UIView {
func anchor(top: NSLayoutYAxisAnchor?, left: NSLayoutXAxisAnchor?,  bottom: NSLayoutYAxisAnchor?, right: NSLayoutXAxisAnchor?, paddingTop: CGFloat, paddingLeft: CGFloat, paddingBottom: CGFloat, paddingRight: CGFloat, height: CGFloat, width: CGFloat){
    
    translatesAutoresizingMaskIntoConstraints = false
    
    if let top = top {
        self.topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true
    }
    
    if let left = left {
        self.leftAnchor.constraint(equalTo: left, constant: paddingLeft).isActive = true
    }
    
    if let bottom = bottom {
        bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true
    }
    
    if let right = right {
        rightAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true
    }
    
    if width != 0 {
        widthAnchor.constraint(equalToConstant: width).isActive = true
    }
    
    if height != 0 {
        heightAnchor.constraint(equalToConstant: height).isActive = true
    }
    }
}


```

#ステップ４: レイアウトを作成
ステップ３で作成したCellをFrimaController上に登録し、レイアウトを作成します。

```Swift

class FrimaController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
    
    let frimaItemCellId = "frimaItemCellId"
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        collectionView.backgroundColor = .white

        //ステップ3で作成したFrimaCellを登録
        collectionView.register(FrimaItemCell.self, forCellWithReuseIdentifier: frimaItemCellId)
        
    }
    
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 20
    }
    
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: frimaItemCellId, for: indexPath) as! FrimaItemCell
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: view.frame.width / 3 - 4, height: view.frame.width / 3)
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 4)
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return 4
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return 0
    }
    
}
```

#完成
これで完成。
<img width="453" alt="スクリーンショット 0002-07-01 午後5.34.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323176/8c1b1401-8375-8f67-fd6a-1eef38443d59.png">

#最後に
今は全てのアイテムが"Tシャツ"、と¥1,000そして画像がありませんが、
次回はアイテムクラスを作り個々のアイテムを設定していきます。

最後に一句(初心者時代にやっていたNG行動)
テラテイル 聞きすぎ怒られ 気がメイル