티스토리 뷰

collectView 코드로 짜기

import UIKit

class ViewController: UIViewController {
    
    var colors: [UIColor] = [
        .systemRed, .systemPurple, .systemBlue, .systemCyan, .systemGray, .systemMint, .systemPink, .systemBrown, .systemIndigo, .systemOrange, .systemYellow, .systemRed, .systemPurple, .systemBlue, .systemCyan, .systemGray, .systemMint, .systemPink, .systemBrown, .systemIndigo, .systemOrange, .systemYellow, .systemGreen, .systemRed
    ]
    
    let flowLayout = UICollectionViewFlowLayout()
    lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
    // 생성에 순서를 보장받을 수 없기 때문에 collectionView를 늦게 만들어 주면 됨(lazy var). 자동 완성이 안되는 걸 보면 알 수 있음.
                                               
    override func viewDidLoad() {
        super.viewDidLoad()
        configureUI()
    }
}

// MARK: - UICollectionViewDataSource
extension ViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return colors.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier, for: indexPath) as? CustomCollectionViewCell else { fatalError() }
        cell.backgroundColor = colors[indexPath.item]
        cell.label.text = "\(indexPath.item)"
        return cell
    }
}

// MARK: - UI
extension ViewController {
    final private func configureUI() {
        setFlowLayout()
        setConstraints()
        addGesture()
    }
    
    final private func setFlowLayout() {
        flowLayout.itemSize = CGSize(width: view.frame.width / 3.3, height: view.frame.height / 4)
        flowLayout.minimumLineSpacing = 20 // 아이템 간의 세로 거리
        flowLayout.minimumInteritemSpacing = 10 // 세로 스크롤에서 아이템 간에 가로 거리
        flowLayout.sectionInset = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4)
//        flowLayout.scrollDirection = .horizontal
    }
    
    final private func setConstraints() {
        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
        
        view.addSubview(collectionView)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
}

 

 

collectionView를 생성할 때는 layout이 필요한데, collectionView의 생성 시점에 layout이 생성된다는 것을 보장할 수 없음(자동 완성이 되지 않음) -> collectionView를 lazy var로 선언

 


CollectionViewCell 옮기기

1. collectionView에 UILongPressGesture를 add해주기

    final private func addGesture() {
        let gesture = UILongPressGestureRecognizer(target: self, action: #selector(didLongPressed(_:)))
        collectionView.addGestureRecognizer(gesture)
    }

 

2. sender를 통해 다양한 값을 받아올 수 있음 (location, state 등)

   @objc func didLongPressed(_ sender: UILongPressGestureRecognizer) {
        let location = sender.location(in: collectionView)
        print("location:",location)
        
        
        switch sender.state {
        case .began:
            print("began")
            guard let indexPath = collectionView.indexPathForItem(at: location) else { return } //indexPath가 존재하지 않는 경우도 있음
            collectionView.beginInteractiveMovementForItem(at: indexPath)
            print("indexPath:", indexPath)
        case .changed:
            print("changed")
            collectionView.updateInteractiveMovementTargetPosition(location)
        case .ended:
            print("ended")
            collectionView.endInteractiveMovement()
        case .cancelled:
            // 전화가 오거나, 알림문자가 오거나 했을 때
            collectionView.cancelInteractiveMovement()
            print("cancelled")
        default:
            break
        }
    }

.began에서 option binding을 하는 이유는 cell 이외에 곳에서 gesture가 발생할 시에는 nil이 발생하기 때문에

 

3. UICollectionViewDelegate 설정

별다른 코드를 치지 않더라도 해당 메서드를 선언하는 것만으로도 LongPressGesture를 통해 cell 이동이 가능

extension ViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {

    }
}

4. 셀 이동 시 발생하는 cell 변경에 대한 대응

상단의 셀을 옮기고 나서 스크롤을 아래로 내렸다 다시 올라가면 옮겼던 셀이 다시 제자리에 있음. 셀의 위치를 바꿨다 하더라도 셀의 재사용성 때문에 상단으로 다시 올라갔을 때 셀이 다시 그려지고, 그 때 원래 배열대로 그려지기 때문에 변하지 않는 것

-> 배열 자체도 셀의 이동과 함께 변경을 해주면 해당 문제 해결됨.

-> 옮겨진 요소(여기서는 컬러)를 해당 배열에서 제거한 후, 옮겨진 자리(indexPath)로 다시 컬러를 insert하면 됨

extension ViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        // 셀의 재사용성 때문에 셀의 위치를 바꿨다고 하더라도, 다시 위로 올렸을 때 원래 배열에 따라 다시 셀이 조정되기 때문에 변화가 반영되지 않는다.
        // 옮긴 indexPath를 반영하여 배열을 변경
        let color = colors.remove(at: sourceIndexPath.item) // 배열에서 아이템을 빼서
        colors.insert(color, at: destinationIndexPath.item)
    }
}

Delegate Pattern

A뷰컨에서 B뷰컨으로 정보를 전달하는 것은 쉽지만, 다시 B에서 A로 정보를 역전달하는 것은 쉽지 않음. 이 때 delegate pattern을 사용하면 됨

 

 

1. 정보를 전달하는 뷰컨 (여기서는 secondVC)

- 정보를 전달하는 쪽에서 protocol과 delegate를 선언해주면 됨. 

- modal 형식으로 present된 secondVC가 dismiss될 때 정보를 전달하면 되므로, deinit 시점에 정보를 전달함

- 해당 시점(deinit)에 weak var로 선언한 delegate에게 프로토콜의 요구사항(didDismissed 메서드)을 실행시킴 (delegate야 이 메서드를 실행해줘1!!)

import UIKit

protocol SecondViewControllerDelegate: AnyObject {
    func didDismissed(text: String)
}

class SecondViewController: UIViewController {
    
    let label = UILabel()
    
    weak var delegate: SecondViewControllerDelegate?

    override func viewDidLoad() {
        super.viewDidLoad()
        configureUI()
        view.backgroundColor = .yellow
    }
    
    deinit {
        delegate?.didDismissed(text: "🍎2번 뷰컨에서 건너옴")
    }
}

//MARK: -UI
extension SecondViewController {
    final private func configureUI() {
        setAttributes()
        setConstraints()
    }
    
    final private func setAttributes() {
        label.text = "나는 두 번째 뷰컨"
    }
    
    final private func setConstraints() {
        view.addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])
    }
}

 

2. 정보를 받는 뷰컨 (여기서는 firstVC)

- firstVC는 secondVC의 delegate 변수를 self로 선언 (secondeVC.label.text = "바보"와 똑같은 동작임. secondVC의 delegate 변수는 바로 firstVC = self 라고 선언해주는 것)

- delegate를 self로 선언했기 때문에 해당 프로토콜을 선언하고, 해당 메서드를 반드시 구현해야 함 (textField, tableView, collectionView 등의 delegate와 동일하게 작동)

- firstVC가 delegate기 secondVC가 deinit 시점에 didMisssed가 불리면 firstVC에서 해당 메서드가 실행됨.

import UIKit

class FirstViewController: UIViewController {
    
    let label = UILabel()
    let button = UIButton(type: .system)

    override func viewDidLoad() {
        super.viewDidLoad()
        configureUI()
    }
}

extension FirstViewController: SecondViewControllerDelegate {
    func didDismissed(text: String) {
        label.text = text
    }
}

//MARK: -UI
extension FirstViewController {
    final private func configureUI() {
        setAttributes()
        setConstraints()
    }
    
    final private func setAttributes() {
        label.text = "나는 첫 번째 뷰컨"
        button.setTitle("2번 뷰컨으로", for: .normal)
        button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
    }
    
    @objc func buttonTapped(_ sender: UIButton) {
        let secondVC = SecondViewController()
        secondVC.delegate = self
        
        present(secondVC, animated: true)
    }
    
    final private func setConstraints() {
        view.addSubview(label)
        view.addSubview(button)
        label.translatesAutoresizingMaskIntoConstraints = false
        button.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            
            button.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 20),
            button.widthAnchor.constraint(equalToConstant: 100),
            button.heightAnchor.constraint(equalToConstant: 30),
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            
        ])
    }
}

 

delegate = self 선언을 반드시 연결되는 시점(?, 버튼을 누른다거나, 다음 화면으로 넘어간다거나)에만 해야한다는 착각을 하고 있었음. 그 시점은 언제든지 될 수 있음. 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크