//
// ViewController.swift
// uikitPractice
//
// Created by bang_hyeonseok on 11/28/23.
//
import UIKit
class MainViewController: UIViewController {
var dataManager = BlockDataManager()
lazy var dateLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
let today = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .none
dateFormatter.dateFormat = "yyyy-MM-dd"
let dateStr = dateFormatter.string(from: today)
label.text = dateStr
return label
}()
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.sectionInset = UIEdgeInsets(top: 5, left: 0, bottom: 5, right: 0)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = .white // 배경색 설정
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
title = "Today"
// navigationController?.navigationBar.prefersLargeTitles = true
view.addSubview(dateLabel)
view.addSubview(collectionView)
NSLayoutConstraint.activate([
dateLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),
dateLabel.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
collectionView.topAnchor.constraint(equalTo: dateLabel.bottomAnchor, constant: 10),
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
])
}
}
extension MainViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return dataManager.itemSections.count
}
// 섹션안의 item갯수
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataManager.itemSections[section].itemSection.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
// 셀 커스터마이징
let view = UIView()
let titleLabel = UILabel()
view.translatesAutoresizingMaskIntoConstraints = false
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.font = UIFont.boldSystemFont(ofSize: 20)
cell.contentView.addSubview(view)
view.addSubview(titleLabel)
titleLabel.textColor = .black
let safeAreaLayoutGuide = cell.contentView.safeAreaLayoutGuide
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
view.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
view.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 10),
view.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -10),
titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
])
let item = dataManager.itemSections[indexPath.section].itemSection[indexPath.row]
if item.itemName == "" {
titleLabel.text = "오늘의 블럭을 추가해주세요"
} else {
titleLabel.text = item.itemName
}
// let item = ItemSection.itemSections[indexPath.section].itemSection[indexPath.row]
view.layer.cornerRadius = 10
view.backgroundColor = .systemGray5
return cell
}
}
extension MainViewController: UICollectionViewDelegateFlowLayout {
// 각 섹션에서 셀 간의 최소 수직 간격을 결정
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 1
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// 섹션의 높이 = view.bounds.height * 0.06 * 셀갯수
let sectionCount = CGFloat(dataManager.itemSections.count)
print("여기 또 타는지 보자구:: \(sectionCount)")
let height = (collectionView.bounds.height - (2 * sectionCount * 5)) / sectionCount
return CGSize(width: collectionView.bounds.width, height: height)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("didSelectItemAt: \(indexPath)")
let count = dataManager.itemSections.count
let newItem = ItemModel(itemName: "block\(count+1)")
dataManager.addItemToSection(indexPath.section, item: newItem)
// 레이아웃 무효화 및 데이터 리로드
// collectionView.collectionViewLayout.invalidateLayout()
collectionView.reloadData()
}
}
class BlockDataManager {
var itemSections: [ItemSection] = [
ItemSection(itemSection: [
ItemModel(itemName: "오늘의 블럭을 추가해주세요")
])
]
func addItemToSection(_ section: Int, item: ItemModel) {
if itemSections.count < 6 {
itemSections.append(ItemSection(itemSection: [item]))
}
}
}
import UIKit
// 1. 데이터 모델 정의
struct Item: Hashable {
let id = UUID()
let title: String
}
enum Section {
case main
}
class MainViewController: UIViewController {
// 2. UICollectionView 및 DataSource 정의
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.sectionInset = UIEdgeInsets(top: 5, left: 0, bottom: 5, right: 0)
let collectionView = UICollectionView(frame: .zero,
collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.delegate = self
collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
return collectionView
}()
lazy var dateLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
let today = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .none
dateFormatter.dateFormat = "yyyy-MM-dd"
let dateStr = dateFormatter.string(from: today)
label.text = dateStr
return label
}()
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
title = "Today"
setupCollectionView()
}
private func setupCollectionView() {
view.addSubview(dateLabel)
view.addSubview(collectionView)
NSLayoutConstraint.activate([
dateLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),
dateLabel.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
collectionView.topAnchor.constraint(equalTo: dateLabel.bottomAnchor, constant: 10),
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
])
// 3. DataSource 및 기타 설정
configureDataSource()
}
private func configureDataSource() {
// 5. DataSource 설정 및 셀 구성
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
(collectionView, indexPath, item) -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier, for: indexPath) as! CustomCollectionViewCell
cell.configure(with: item.title)
return cell
}
applyInitialSnapshots() // 초기 데이터 로드
}
private func applyInitialSnapshots() {
// 6. 초기 데이터 스냅샷 적용
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems([
Item(title: "Block 1"),
])
dataSource.apply(snapshot, animatingDifferences: false)
}
}
extension MainViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// 현재 스냅샷에서 전체 아이템의 수를 계산
let itemCount = dataSource.snapshot().numberOfItems
// 각 섹션의 높이를 계산
let sectionHeight = (collectionView.bounds.height - (2 * CGFloat(itemCount) * 5)) / CGFloat(itemCount)
// 셀의 크기 반환
return CGSize(width: collectionView.bounds.width, height: sectionHeight)
}
}
extension MainViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// 현재 스냅샷의 아이템 수
let currentSnapshot = dataSource.snapshot()
let itemCount = currentSnapshot.numberOfItems
// 새로운 아이템 생성 (아이템 개수 + 1)
let newItem = Item(title: "Block \(itemCount + 1)")
// 새 스냅샷에 아이템 추가
var newSnapshot = currentSnapshot
newSnapshot.appendItems([newItem], toSection: .main)
// 스냅샷 적용
// dataSource.apply(newSnapshot, animatingDifferences: true)
// // 레이아웃 강제 업데이트
// collectionView.collectionViewLayout.invalidateLayout()
// 스냅샷 적용 및 전체 뷰 갱신
dataSource.apply(newSnapshot, animatingDifferences: true) {
// 모든 셀을 새로 그리도록 컬렉션 뷰에 지시
// collectionView.reloadData()
}
}
}
import UIKit
class CustomCollectionViewCell: UICollectionViewCell {
static let identifier = "CustomCollectionViewCell"
lazy var titleLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = .black
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
lazy var containerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemGray
view.layer.cornerRadius = 30
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(containerView)
containerView.addSubview(titleLabel)
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: self.contentView.topAnchor),
containerView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
containerView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 10),
containerView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -10),
titleLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
titleLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
titleLabel.text = nil // 텍스트 초기화
}
func configure(with title: String) {
titleLabel.text = title
}
}
//
// ViewController.swift
// CollectionViewApp
//
// Created by bang_hyeonseok on 12/5/23.
//
import UIKit
// 1. 데이터 모델 정의
struct Item: Hashable {
let id = UUID()
let title: String
// Add this static property
static let initialData: [Item] = [
Item(title: "Block 1"),
Item(title: "Block 2"),
]
}
enum Section {
case main
}
class MainViewController: UIViewController {
// 2. UICollectionView 및 DataSource 정의
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
let collectionView = UICollectionView(frame: .zero,
collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.delegate = self
collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
return collectionView
}()
lazy var dateLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
let today = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .none
dateFormatter.dateFormat = "yyyy-MM-dd"
let dateStr = dateFormatter.string(from: today)
label.text = dateStr
return label
}()
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
title = "Today"
setupCollectionView()
}
private func setupCollectionView() {
view.addSubview(dateLabel)
view.addSubview(collectionView)
NSLayoutConstraint.activate([
dateLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),
dateLabel.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
collectionView.topAnchor.constraint(equalTo: dateLabel.bottomAnchor, constant: 10),
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
])
// 3. DataSource 및 기타 설정
configureDataSource()
}
private func configureDataSource() {
// 5. DataSource 설정 및 셀 구성
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
(collectionView, indexPath, item) -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier, for: indexPath) as! CustomCollectionViewCell
cell.configure(with: item.title)
return cell
}
applyInitialSnapshots() // 초기 데이터 로드
}
private func applyInitialSnapshots() {
// 6. 초기 데이터 스냅샷 적용
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
// snapshot.appendItems([
// Item(title: "Block 1"),
// ])
snapshot.appendItems(Item.initialData) // Use the static property here
dataSource.apply(snapshot, animatingDifferences: false)
}
}
extension MainViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// 현재 스냅샷에서 전체 아이템의 수를 계산
let itemCount = dataSource.snapshot().numberOfItems
// 각 섹션의 높이를 계산
let sectionHeight = (collectionView.bounds.height - (2 * CGFloat(itemCount) * 5)) / CGFloat(itemCount)
// 셀의 크기 반환
return CGSize(width: collectionView.bounds.width, height: sectionHeight)
}
}
extension MainViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// 현재 스냅샷의 아이템 수
let currentSnapshot = dataSource.snapshot()
let itemCount = currentSnapshot.numberOfItems
// 새로운 아이템 생성 (아이템 개수 + 1)
let newItem = Item(title: "Block \(itemCount + 1)")
// 새 스냅샷에 아이템 추가
var newSnapshot = currentSnapshot
newSnapshot.appendItems([newItem], toSection: .main)
dataSource.apply(newSnapshot, animatingDifferences: true) {
}
}
}
import UIKit
class CustomCollectionViewCell: UICollectionViewCell {
static let identifier = "CustomCollectionViewCell"
lazy var titleLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = .black
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
lazy var containerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemGray
view.layer.cornerRadius = 30
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(containerView)
containerView.addSubview(titleLabel)
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: self.contentView.topAnchor),
containerView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
containerView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 10),
containerView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -10),
titleLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
titleLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
titleLabel.text = nil // 텍스트 초기화
}
func configure(with title: String) {
titleLabel.text = title
}
}
import UIKit
// 1. 데이터 모델 정의
struct Item: Hashable {
let id = UUID()
let title: String
// Add this static property
static let initialData: [Item] = [
Item(title: "Block 1"),
Item(title: "Block 2"),
]
}
enum Section {
case main
}
class MainViewController: UIViewController {
// 2. UICollectionView 및 DataSource 정의
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
let collectionView = UICollectionView(frame: .zero,
collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.delegate = self
collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
return collectionView
}()
lazy var dateLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
let today = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .none
dateFormatter.dateFormat = "yyyy-MM-dd"
let dateStr = dateFormatter.string(from: today)
label.text = dateStr
return label
}()
lazy var plusBtn: UIButton = {
var config = UIButton.Configuration.plain()
// UIAction 생성
let action = UIAction { [weak self] _ in
// 버튼이 탭될 때 실행할 코드
guard let self else { return }
checkAddAction()
}
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 20, weight: .bold, scale: .large) // 이미지의 크기와 스케일을 조정
config.image = UIImage(systemName: "plus", withConfiguration: symbolConfig)
let button = UIButton(configuration: config, primaryAction: action)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
lazy var minusBtn: UIButton = {
var config = UIButton.Configuration.plain()
// UIAction 생성
let action = UIAction { [weak self] _ in
// 버튼이 탭될 때 실행할 코드
guard let self else { return }
checkMinusAction()
}
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 20, weight: .bold, scale: .large) // 이미지의 크기와 스케일을 조정
config.image = UIImage(systemName: "minus", withConfiguration: symbolConfig)
let button = UIButton(configuration: config, primaryAction: action)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private func toggleButtonColor(targetBtn: UIButton, _ value: Bool) {
if var config = targetBtn.configuration {
// 색상을 systemRed로 설정
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 20, weight: .bold, scale: .large)
let imageName = targetBtn == plusBtn ? "plus" : "minus"
config.image = UIImage(systemName: imageName, withConfiguration: symbolConfig)?
.withTintColor(value ? .systemBlue : .lightGray, renderingMode: .alwaysOriginal)
targetBtn.configuration = config
}
}
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
title = "Today"
setupCollectionView()
}
private func setupCollectionView() {
view.addSubview(dateLabel)
view.addSubview(minusBtn)
view.addSubview(plusBtn)
view.addSubview(collectionView)
NSLayoutConstraint.activate([
dateLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),
dateLabel.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
minusBtn.heightAnchor.constraint(equalToConstant: 40),
minusBtn.widthAnchor.constraint(equalToConstant: 40),
minusBtn.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10),
minusBtn.centerYAnchor.constraint(equalTo: dateLabel.centerYAnchor),
plusBtn.heightAnchor.constraint(equalToConstant: 40),
plusBtn.widthAnchor.constraint(equalToConstant: 40),
plusBtn.trailingAnchor.constraint(equalTo: minusBtn.leadingAnchor),
plusBtn.centerYAnchor.constraint(equalTo: dateLabel.centerYAnchor),
collectionView.topAnchor.constraint(equalTo: dateLabel.bottomAnchor, constant: 10),
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
])
// 3. DataSource 및 기타 설정
configureDataSource()
}
private func configureDataSource() {
// 5. DataSource 설정 및 셀 구성
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
(collectionView, indexPath, item) -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier, for: indexPath) as! CustomCollectionViewCell
cell.configure(with: item.title)
return cell
}
applyInitialSnapshots() // 초기 데이터 로드
}
private func applyInitialSnapshots() {
// 6. 초기 데이터 스냅샷 적용
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
// snapshot.appendItems([
// Item(title: "Block 1"),
// ])
snapshot.appendItems(Item.initialData) // Use the static property here
dataSource.apply(snapshot, animatingDifferences: false)
}
/// 마이너스 로직
private func checkMinusAction() {
print(#function)
// 현재 스냅샷의 아이템 수
let currentSnapshot = dataSource.snapshot()
let itemCount = currentSnapshot.numberOfItems
if itemCount >= 2 {
minusItem()
}
}
/// 마이너스 아이템 및 UI업데이트
private func minusItem() {
var currentSnapshot = dataSource.snapshot()
if !currentSnapshot.itemIdentifiers.isEmpty {
// Remove the last item
currentSnapshot.deleteItems([currentSnapshot.itemIdentifiers.last!])
// Apply the updated snapshot
dataSource.apply(currentSnapshot, animatingDifferences: true)
// Additional UI updates (if needed)
DispatchQueue.main.async {
let updatedItemCount = self.dataSource.snapshot().numberOfItems
if updatedItemCount == 1 {
self.toggleButtonColor(targetBtn: self.minusBtn, false)
self.minusBtn.isEnabled = false
}
self.toggleButtonColor(targetBtn: self.plusBtn, true)
self.plusBtn.isEnabled = true
}
}
}
private func checkAddAction() {
print(#function)
// 현재 스냅샷의 아이템 수
let currentSnapshot = dataSource.snapshot()
let itemCount = currentSnapshot.numberOfItems
if itemCount < 6 {
// 새로운 아이템 생성 (아이템 개수 + 1)
setTextAlert()
}
}
private func setTextAlert() {
// Alert Controller 생성
let alertController = UIAlertController(title: "", message: "블록이름을 입력하세요", preferredStyle: .alert)
// TextField 추가
alertController.addTextField { textField in
textField.placeholder = "제목"
}
let addAction = UIAlertAction(title: "추가", style: .default) { [weak self, weak alertController] _ in
guard let self = self,
let alertController = alertController,
let textField = alertController.textFields?.first,
let text = textField.text, !text.isEmpty else { return }
// 새 Item 생성 및 추가
let newItem = Item(title: text)
self.addNewItem(item: newItem)
}
// '취소' 액션
let cancelAction = UIAlertAction(title: "취소", style: .cancel)
// 액션을 Alert Controller에 추가
alertController.addAction(addAction)
alertController.addAction(cancelAction)
// Alert 표시
DispatchQueue.main.async {
self.present(alertController, animated: true)
}
}
private func addNewItem(item: Item) {
// 현재 스냅샷의 아이템 수
let currentSnapshot = dataSource.snapshot()
// 새 스냅샷에 아이템 추가
var newSnapshot = currentSnapshot
newSnapshot.appendItems([item], toSection: .main)
dataSource.apply(newSnapshot, animatingDifferences: true) {
print("된거??")
DispatchQueue.main.async {
let currentSnapshot = self.dataSource.snapshot()
print("몇개야 :\(currentSnapshot.numberOfItems)")
if currentSnapshot.numberOfItems == 6 {
self.toggleButtonColor(targetBtn: self.plusBtn, false)
self.plusBtn.isEnabled = false
}
self.toggleButtonColor(targetBtn: self.minusBtn, true)
self.minusBtn.isEnabled = true
}
}
}
}
extension MainViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// 현재 스냅샷에서 전체 아이템의 수를 계산
let itemCount = dataSource.snapshot().numberOfItems
// 각 섹션의 높이를 계산
let sectionHeight = (collectionView.bounds.height - (2 * CGFloat(itemCount) * 5)) / CGFloat(itemCount)
// 셀의 크기 반환
return CGSize(width: collectionView.bounds.width, height: sectionHeight)
}
}
extension MainViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("indexPath: \(indexPath)")
}
}
import UIKit
class CustomCollectionViewCell: UICollectionViewCell {
static let identifier = "CustomCollectionViewCell"
lazy var titleLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = .black
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
lazy var containerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemYellow
view.layer.cornerRadius = 30
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(containerView)
containerView.addSubview(titleLabel)
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: self.contentView.topAnchor),
containerView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
containerView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 10),
containerView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -10),
titleLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
titleLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
titleLabel.text = nil // 텍스트 초기화
}
func configure(with title: String) {
titleLabel.text = title
}
}
////
//// ViewController.swift
//// CollectionViewApp
////
//// Created by bang_hyeonseok on 12/5/23.
////
import UIKit
// 1. 데이터 모델 정의
struct Item: Hashable {
let id = UUID()
let title: String
// Add this static property
static let initialData: [Item] = [
Item(title: "Block 1"),
Item(title: "Block 2"),
]
}
enum Section {
case main
}
// MARK: - ViewController
class MainViewController: UIViewController {
// 2. UICollectionView 및 DataSource 정의
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
let collectionView = UICollectionView(frame: .zero,
collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.delegate = self
// collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
collectionView.register(CustomCollectionViewListCell.self, forCellWithReuseIdentifier: CustomCollectionViewListCell.identifier)
return collectionView
}()
lazy var dateLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
let today = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .none
dateFormatter.dateFormat = "yyyy-MM-dd"
let dateStr = dateFormatter.string(from: today)
label.text = dateStr
return label
}()
lazy var plusBtn: UIButton = {
var config = UIButton.Configuration.plain()
// UIAction 생성
let action = UIAction { [weak self] _ in
// 버튼이 탭될 때 실행할 코드
guard let self else { return }
checkAddAction()
}
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 20, weight: .bold, scale: .large) // 이미지의 크기와 스케일을 조정
config.image = UIImage(systemName: "plus", withConfiguration: symbolConfig)
let button = UIButton(configuration: config, primaryAction: action)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
// 편집 버튼 추가
lazy var editButton: UIBarButtonItem = {
let button = UIBarButtonItem(title: "Edit",
style: .plain,
target: self, action: #selector(toggleEditMode))
return button
}()
lazy var minusBtn: UIButton = {
var config = UIButton.Configuration.plain()
// UIAction 생성
let action = UIAction { [weak self] _ in
// 버튼이 탭될 때 실행할 코드
guard let self else { return }
checkMinusAction()
}
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 20, weight: .bold, scale: .large) // 이미지의 크기와 스케일을 조정
config.image = UIImage(systemName: "minus", withConfiguration: symbolConfig)
let button = UIButton(configuration: config, primaryAction: action)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
// 편집 모드 플래그
var isEditingMode = false
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
title = "Today"
navigationItem.rightBarButtonItem = editButton
setupCollectionView()
}
private func setupCollectionView() {
view.addSubview(dateLabel)
view.addSubview(plusBtn)
view.addSubview(collectionView)
NSLayoutConstraint.activate([
dateLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),
dateLabel.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
plusBtn.heightAnchor.constraint(equalToConstant: 40),
plusBtn.widthAnchor.constraint(equalToConstant: 40),
plusBtn.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10),
plusBtn.centerYAnchor.constraint(equalTo: dateLabel.centerYAnchor),
collectionView.topAnchor.constraint(equalTo: dateLabel.bottomAnchor, constant: 10),
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
])
// 3. DataSource 및 기타 설정
configureDataSource()
}
private func configureDataSource() {
// 5. DataSource 설정 및 셀 구성
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
(collectionView, indexPath, item) -> UICollectionViewListCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewListCell.identifier, for: indexPath) as! CustomCollectionViewListCell
cell.configure(with: item.title)
return cell
}
applyInitialSnapshots() // 초기 데이터 로드
}
private func applyInitialSnapshots() {
// 6. 초기 데이터 스냅샷 적용
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(Item.initialData) // Use the static property
dataSource.apply(snapshot, animatingDifferences: false)
}
@objc private func toggleEditMode() {
isEditingMode = !isEditingMode
collectionView.isEditing = isEditingMode
editButton.title = isEditingMode ? "Done" : "Edit"
collectionView.performBatchUpdates(nil)
}
private func toggleButtonColor(targetBtn: UIButton, _ value: Bool) {
if var config = targetBtn.configuration {
// 색상을 systemRed로 설정
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 20, weight: .bold, scale: .large)
let imageName = targetBtn == plusBtn ? "plus" : "minus"
config.image = UIImage(systemName: imageName,
withConfiguration: symbolConfig)?
.withTintColor(value ? .systemBlue : .lightGray,
renderingMode: .alwaysOriginal)
targetBtn.configuration = config
}
}
/// 마이너스 로직
private func checkMinusAction() {
print(#function)
// 현재 스냅샷의 아이템 수
let currentSnapshot = dataSource.snapshot()
let itemCount = currentSnapshot.numberOfItems
if itemCount >= 2 {
setDeleteAlert()
}
}
/// 마이너스 아이템 및 UI업데이트
private func minusItem() {
var currentSnapshot = dataSource.snapshot()
if !currentSnapshot.itemIdentifiers.isEmpty {
// Remove the last item
currentSnapshot.deleteItems([currentSnapshot.itemIdentifiers.last!])
// Apply the updated snapshot
dataSource.apply(currentSnapshot, animatingDifferences: true)
// Additional UI updates (if needed)
DispatchQueue.main.async {
let updatedItemCount = self.dataSource.snapshot().numberOfItems
if updatedItemCount == 1 {
self.toggleButtonColor(targetBtn: self.minusBtn, false)
self.minusBtn.isEnabled = false
}
self.toggleButtonColor(targetBtn: self.plusBtn, true)
self.plusBtn.isEnabled = true
}
}
}
private func checkAddAction() {
print(#function)
// 현재 스냅샷의 아이템 수
let currentSnapshot = dataSource.snapshot()
let itemCount = currentSnapshot.numberOfItems
if itemCount < 6 {
// 새로운 아이템 생성 (아이템 개수 + 1)
setTextAlert()
}
}
private func setTextAlert() {
// Alert Controller 생성
let alertController = UIAlertController(title: "", message: "블록이름을 입력하세요", preferredStyle: .alert)
// TextField 추가
alertController.addTextField { textField in
textField.placeholder = "제목"
}
let addAction = UIAlertAction(title: "확인", style: .default) { [weak self, weak alertController] _ in
guard let self = self,
let alertController = alertController,
let textField = alertController.textFields?.first,
let text = textField.text, !text.isEmpty else { return }
// 새 Item 생성 및 추가
let newItem = Item(title: text)
self.addNewItem(item: newItem)
}
// '취소' 액션
let cancelAction = UIAlertAction(title: "취소", style: .destructive)
alertController.addAction(cancelAction)
alertController.addAction(addAction)
// Alert 표시
DispatchQueue.main.async {
self.present(alertController, animated: true)
}
}
private func setDeleteAlert() {
// Alert Controller 생성
let alertController = UIAlertController(title: "", message: "정말로 삭제하시겠습니까", preferredStyle: .alert)
let addAction = UIAlertAction(title: "확인", style: .default) { [weak self] _ in
guard let self else { return }
minusItem()
}
// '취소' 액션
let cancelAction = UIAlertAction(title: "취소", style: .destructive)
alertController.addAction(cancelAction)
alertController.addAction(addAction)
// Alert 표시
DispatchQueue.main.async {
self.present(alertController, animated: true)
}
}
private func addNewItem(item: Item) {
// 현재 스냅샷의 아이템 수
let currentSnapshot = dataSource.snapshot()
// 새 스냅샷에 아이템 추가
var newSnapshot = currentSnapshot
newSnapshot.appendItems([item], toSection: .main)
dataSource.apply(newSnapshot, animatingDifferences: true) {
DispatchQueue.main.async {
let currentSnapshot = self.dataSource.snapshot()
print("몇개야 :\(currentSnapshot.numberOfItems)")
if currentSnapshot.numberOfItems == 6 {
self.toggleButtonColor(targetBtn: self.plusBtn, false)
self.plusBtn.isEnabled = false
}
self.toggleButtonColor(targetBtn: self.minusBtn, true)
self.minusBtn.isEnabled = true
}
}
}
}
extension MainViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// 현재 스냅샷에서 전체 아이템의 수를 계산
let itemCount = dataSource.snapshot().numberOfItems
// 각 섹션의 높이를 계산
let sectionHeight = (collectionView.bounds.height - (2 * CGFloat(itemCount) * 5)) / CGFloat(itemCount)
// 셀의 크기 반환
return CGSize(width: collectionView.bounds.width, height: sectionHeight)
}
}
extension MainViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("indexPath: \(indexPath)")
if let cell = collectionView.cellForItem(at: indexPath) as? CustomCollectionViewListCell {
// cell.isSelected = false // 셀의 선택 상태 해제
print("cell.isSelected: \(cell.isSelected)")
cell.backgroundColor = .clear
}
}
func collectionView(_ collectionView: UICollectionView, canEditItemAt indexPath: IndexPath) -> Bool {
// 모든 셀이 편집 가능하게 설정
return true
}
func collectionView(_ collectionView: UICollectionView, commit editingStyle: UITableViewCell.EditingStyle, forItemAt indexPath: IndexPath) {
if editingStyle == .delete {
// 셀 삭제 로직 구현
var snapshot = dataSource.snapshot()
if let item = dataSource.itemIdentifier(for: indexPath) {
snapshot.deleteItems([item])
dataSource.apply(snapshot)
}
}
}
}
class CustomCollectionViewListCell: UICollectionViewListCell {
static let identifier = "CustomCollectionViewListCell"
lazy var titleLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = .black
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
lazy var containerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemGray5
view.layer.cornerRadius = 30
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .clear
contentView.addSubview(containerView)
containerView.addSubview(titleLabel)
print("되는거야2")
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: self.contentView.topAnchor),
containerView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
containerView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 10),
containerView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -10),
titleLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
titleLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
titleLabel.text = nil // 텍스트 초기화
}
func configure(with title: String) {
titleLabel.text = title
}
override func updateConfiguration(using state: UICellConfigurationState) {
super.updateConfiguration(using: state)
// 편집 모드에 따라 액세서리 설정
if state.isEditing {
// 편집 모드일 때 삭제 버튼을 표시
accessories = [.delete(displayed: .whenEditing, actionHandler: { [weak self] in
// 삭제 로직 구현
self?.handleDeleteAction()
})]
} else {
// 편집 모드가 아닐 때는 액세서리 제거
accessories = []
}
}
private func handleDeleteAction() {
print(#function)
// 삭제 처리 로직
}
}
// 1. 데이터 모델 정의
struct Item: Hashable {
let id = UUID()
let title: String
// Add this static property
static let initialData: [Item] = [
Item(title: "Block 1"),
Item(title: "Block 2"),
]
}
enum Section {
case main
}
class MainTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
self.tabBar.backgroundColor = UIColor.systemGray6
let todayVC = UINavigationController(rootViewController: MainViewController())
let calendarVC = UINavigationController(rootViewController: MainViewController())
let settingVC = UINavigationController(rootViewController: MainViewController())
// UIKit, SwiftUI
todayVC.tabBarItem = UITabBarItem(title: "오늘",
image: UIImage(systemName: "house.fill"),
selectedImage: nil)
// Networking,
calendarVC.tabBarItem = UITabBarItem(title: "달력",
image: UIImage(systemName: "square.grid.3x3.fill"),
selectedImage: nil)
settingVC.tabBarItem = UITabBarItem(title: "설정",
image: UIImage(systemName: "gear"),
selectedImage: nil)
self.viewControllers = [
todayVC,
calendarVC,
settingVC,
]
}
}
// MARK: - ViewController
class MainViewController: UIViewController {
// MARK: - Var
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
// 편집 모드 플래그
var isEditingMode = false
// MARK: - UIComponents
// 2. UICollectionView 및 DataSource 정의
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
let collectionView = UICollectionView(frame: .zero,
collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.delegate = self
// collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
collectionView.register(CustomCollectionViewListCell.self, forCellWithReuseIdentifier: CustomCollectionViewListCell.identifier)
return collectionView
}()
lazy var dateLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
let today = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .none
dateFormatter.dateFormat = "yyyy-MM-dd"
let dateStr = dateFormatter.string(from: today)
label.text = dateStr
return label
}()
lazy var plusButton: UIBarButtonItem = {
let action = UIAction { [weak self] _ in
// 버튼이 탭될 때 실행할 코드
self?.checkAddAction()
}
return UIBarButtonItem(systemItem: .add, primaryAction: action)
}()
// 편집 버튼 추가
lazy var editButton: UIBarButtonItem = {
let button = UIBarButtonItem(title: "Edit",
style: .plain,
target: self,
action: #selector(toggleEditMode))
return button
}()
// TODO: 삭제필요
lazy var minusBtn: UIButton = {
var config = UIButton.Configuration.plain()
// UIAction 생성
let action = UIAction { [weak self] _ in
// 버튼이 탭될 때 실행할 코드
guard let self else { return }
checkMinusAction()
}
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 20, weight: .bold, scale: .large)
config.image = UIImage(systemName: "minus", withConfiguration: symbolConfig)
let button = UIButton(configuration: config, primaryAction: action)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private func updatePlusButtonState() {
let itemCount = dataSource.snapshot().numberOfItems
if itemCount >= 6 {
plusButton.tintColor = .lightGray
plusButton.isEnabled = false
} else {
plusButton.tintColor = .systemBlue
plusButton.isEnabled = true
}
}
// MARK: - Functions
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
title = "Today"
navigationItem.rightBarButtonItems = [editButton, plusButton]
setupCollectionView()
updatePlusButtonState()
}
private func setupCollectionView() {
view.addSubview(dateLabel)
view.addSubview(collectionView)
NSLayoutConstraint.activate([
dateLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),
dateLabel.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
collectionView.topAnchor.constraint(equalTo: dateLabel.bottomAnchor, constant: 10),
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
])
// 3. DataSource 및 기타 설정
configureDataSource()
}
private func configureDataSource() {
// 5. DataSource 설정 및 셀 구성
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
(collectionView, indexPath, item) -> UICollectionViewListCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewListCell.identifier, for: indexPath) as! CustomCollectionViewListCell
cell.configure(with: item.title)
cell.delegate = self
let view = UIView()
view.backgroundColor = .clear
cell.selectedBackgroundView = view
return cell
}
applyInitialSnapshots() // 초기 데이터 로드
}
private func applyInitialSnapshots() {
// 6. 초기 데이터 스냅샷 적용
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(Item.initialData) // Use the static property
dataSource.apply(snapshot, animatingDifferences: false)
}
@objc private func toggleEditMode() {
collectionView.isEditing.toggle()
editButton.title = collectionView.isEditing ? "Done" : "Edit"
collectionView.performBatchUpdates(nil)
}
private func toggleButtonColor(targetBtn: UIButton, _ value: Bool) {
if var config = targetBtn.configuration {
// 색상을 systemRed로 설정
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 20, weight: .bold, scale: .large)
// let imageName = targetBtn == plusBtn ? "plus" : "minus"
guard let btnImage = UIImage(systemName: "plus",
withConfiguration: symbolConfig) else { return }
btnImage.withTintColor(value ? .systemBlue : .lightGray,
renderingMode: .alwaysOriginal)
config.image = btnImage
targetBtn.configuration = config
}
}
/// 마이너스 로직
private func checkMinusAction() {
print(#function)
// 현재 스냅샷의 아이템 수
let currentSnapshot = dataSource.snapshot()
let itemCount = currentSnapshot.numberOfItems
if itemCount >= 2 {
setDeleteAlert()
}
}
/// 마이너스 아이템 및 UI업데이트
private func minusItem() {
var currentSnapshot = dataSource.snapshot()
if !currentSnapshot.itemIdentifiers.isEmpty {
// Remove the last item
currentSnapshot.deleteItems([currentSnapshot.itemIdentifiers.last!])
// Apply the updated snapshot
dataSource.apply(currentSnapshot, animatingDifferences: true) {
self.updatePlusButtonState() // 셀 삭제 후 plusButton 상태 업데이트
}
}
}
private func checkAddAction() {
print(#function)
// 현재 스냅샷의 아이템 수
let currentSnapshot = dataSource.snapshot()
let itemCount = currentSnapshot.numberOfItems
if itemCount < 6 {
// 새로운 아이템 생성 (아이템 개수 + 1)
setTextAlert()
}
}
private func setTextAlert() {
// Alert Controller 생성
let alertController = UIAlertController(title: "", message: "블록이름을 입력하세요", preferredStyle: .alert)
// TextField 추가
alertController.addTextField { textField in
textField.placeholder = "제목"
}
let addAction = UIAlertAction(title: "확인", style: .default) { [weak self, weak alertController] _ in
guard let self = self,
let alertController = alertController,
let textField = alertController.textFields?.first,
let text = textField.text, !text.isEmpty else { return }
// 새 Item 생성 및 추가
let newItem = Item(title: text)
self.addNewItem(item: newItem)
}
// '취소' 액션
let cancelAction = UIAlertAction(title: "취소", style: .destructive)
alertController.addAction(cancelAction)
alertController.addAction(addAction)
// Alert 표시
DispatchQueue.main.async {
self.present(alertController, animated: true)
}
}
private func setDeleteAlert() {
// Alert Controller 생성
let alertController = UIAlertController(title: "", message: "정말로 삭제하시겠습니까", preferredStyle: .alert)
let addAction = UIAlertAction(title: "확인", style: .default) { [weak self] _ in
guard let self else { return }
minusItem()
}
// '취소' 액션
let cancelAction = UIAlertAction(title: "취소", style: .destructive)
alertController.addAction(cancelAction)
alertController.addAction(addAction)
// Alert 표시
DispatchQueue.main.async {
self.present(alertController, animated: true)
}
}
private func addNewItem(item: Item) {
// 현재 스냅샷의 아이템 수
let currentSnapshot = dataSource.snapshot()
// 새 스냅샷에 아이템 추가
var newSnapshot = currentSnapshot
newSnapshot.appendItems([item], toSection: .main)
dataSource.apply(newSnapshot, animatingDifferences: true) {
self.updatePlusButtonState()
}
}
}
extension MainViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// 현재 스냅샷에서 전체 아이템의 수를 계산
let itemCount = dataSource.snapshot().numberOfItems
// 각 섹션의 높이를 계산
let sectionHeight = (collectionView.bounds.height - (2 * CGFloat(itemCount) * 5)) / CGFloat(itemCount)
// 셀의 크기 반환
return CGSize(width: collectionView.bounds.width, height: sectionHeight)
}
// func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
// return false
// }
}
extension MainViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("indexPath: \(indexPath)")
if let cell = collectionView.cellForItem(at: indexPath) as? CustomCollectionViewListCell {
// cell.isSelected = false // 셀의 선택 상태 해제
// cell.isHighlighted = false
print("cell.isSelected: \(cell.isSelected)")
// cell.backgroundColor = .clear
print("cell.isHighlighted: \(cell.isHighlighted)")
}
}
}
extension MainViewController: CustomCollectionViewListCellDelegate {
func deleteItem(cell: CustomCollectionViewListCell) {
guard let indexPath = collectionView.indexPath(for: cell),
let item = dataSource.itemIdentifier(for: indexPath) else { return }
var snapshot = dataSource.snapshot()
snapshot.deleteItems([item])
dataSource.apply(snapshot, animatingDifferences: true)
}
}
protocol CustomCollectionViewListCellDelegate: AnyObject {
func deleteItem(cell: CustomCollectionViewListCell)
}
class CustomCollectionViewListCell: UICollectionViewListCell {
static let identifier = "CustomCollectionViewListCell"
weak var delegate: CustomCollectionViewListCellDelegate?
override var isSelected: Bool {
didSet {
print(#function)
}
}
lazy var titleLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = .black
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
lazy var containerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemGray5
view.layer.cornerRadius = 30
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
// self.backgroundColor = .clear
contentView.addSubview(containerView)
containerView.addSubview(titleLabel)
print("되는거야2")
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: self.contentView.topAnchor),
containerView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
containerView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 10),
containerView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -10),
titleLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
titleLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
titleLabel.text = nil // 텍스트 초기화
}
func configure(with title: String) {
titleLabel.text = title
}
override func updateConfiguration(using state: UICellConfigurationState) {
super.updateConfiguration(using: state)
// 편집 모드에 따라 액세서리 설정
if state.isEditing {
// 편집 모드일 때 삭제 버튼을 표시
// accessories = [.delete(displayed: .whenEditing,
// actionHandler: { [weak self] in
// guard let self = self else { return }
// // 삭제 로직 구현
// self.delegate?.deleteItem(cell: self)
// })]
// delete 액션
let deleteAction = UICellAccessory.delete(displayed: .whenEditing) { [weak self] in
print("deleteAction")
guard let self = self else { return }
self.delegate?.deleteItem(cell: self)
}
let reorderAction = UICellAccessory.reorder(displayed: .always,
options: .init(showsVerticalSeparator: false))
accessories = [deleteAction, reorderAction ]
} else {
// 편집 모드가 아닐 때는 액세서리 제거
accessories = []
}
}
}
리스트를 드래그해서 순서를 변경하는 것은 굉장히 흔한 경험이다.
// MARK: - ViewController
class MainViewController: UIViewController {
// MARK: - Var
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
// 편집 모드 플래그
var isEditingMode = false
// MARK: - UIComponents
// 2. UICollectionView 및 DataSource 정의
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
let collectionView = UICollectionView(frame: .zero,
collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.delegate = self
collectionView.register(CustomCollectionViewListCell.self, forCellWithReuseIdentifier: CustomCollectionViewListCell.identifier)
return collectionView
}()
lazy var dateLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
let today = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .none
dateFormatter.dateFormat = "yyyy-MM-dd"
let dateStr = dateFormatter.string(from: today)
label.text = dateStr
return label
}()
lazy var plusButton: UIBarButtonItem = {
let action = UIAction { [weak self] _ in
// 버튼이 탭될 때 실행할 코드
self?.checkAddAction()
}
return UIBarButtonItem(systemItem: .add, primaryAction: action)
}()
// 편집 버튼 추가
lazy var editButton: UIBarButtonItem = {
let button = UIBarButtonItem(title: "Edit",
style: .plain,
target: self,
action: #selector(toggleEditMode))
return button
}()
private func updatePlusButtonState() {
let itemCount = dataSource.snapshot().numberOfItems
if itemCount >= 6 {
plusButton.tintColor = .lightGray
plusButton.isEnabled = false
} else {
plusButton.tintColor = .systemBlue
plusButton.isEnabled = true
}
}
private func updateAllButtonStates() {
let snapshot = dataSource.snapshot()
let itemCount = snapshot.numberOfItems
let isPlusBtnEnable = itemCount < 6
let isEditBtnEnable = !(itemCount == 1 && snapshot.itemIdentifiers.first == Item.tutorialItem)
configureButtonAppearance(button: plusButton, isEnabled: isPlusBtnEnable)
configureButtonAppearance(button: editButton, isEnabled: isEditBtnEnable)
}
private func configureButtonAppearance(button: UIBarButtonItem, isEnabled: Bool) {
button.isEnabled = isEnabled
button.tintColor = isEnabled ? .systemBlue : .lightGray
}
// MARK: - Functions
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
title = "Today"
navigationItem.rightBarButtonItems = [editButton, plusButton]
setupCollectionView()
collectionView.dragDelegate = self
collectionView.dropDelegate = self
collectionView.dragInteractionEnabled = true
loadItems()
// 저장된 데이터가 없으면 초기 데이터 로드
if dataSource.snapshot().numberOfItems == 0 {
applyInitialSnapshots()
}
updateAllButtonStates()
}
private func setupCollectionView() {
view.addSubview(dateLabel)
view.addSubview(collectionView)
NSLayoutConstraint.activate([
dateLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),
dateLabel.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
collectionView.topAnchor.constraint(equalTo: dateLabel.bottomAnchor, constant: 10),
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
])
// 3. DataSource 및 기타 설정
configureDataSource()
}
private func configureDataSource() {
// 5. DataSource 설정 및 셀 구성
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
(collectionView, indexPath, item) -> UICollectionViewListCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewListCell.identifier, for: indexPath) as! CustomCollectionViewListCell
cell.configure(with: item.title)
cell.delegate = self
let view = UIView()
view.backgroundColor = .clear
cell.selectedBackgroundView = view
return cell
}
}
@objc private func toggleEditMode() {
collectionView.isEditing.toggle()
editButton.title = collectionView.isEditing ? "Done" : "Edit"
collectionView.performBatchUpdates(nil)
}
private func checkAddAction() {
print(#function)
// 현재 스냅샷의 아이템 수
let currentSnapshot = dataSource.snapshot()
let itemCount = currentSnapshot.numberOfItems
if itemCount == 1,
let firstItem = currentSnapshot.itemIdentifiers.first,
firstItem.title == Item.initialMessage {
// 첫 번째 아이템이 "내용을 추가해주세요" 메시지일 때 새 아이템 추가
setTextAlert(isFirstItem: true)
} else {
// 기존 로직
setTextAlert(isFirstItem: false)
}
}
private func setTextAlert(isFirstItem: Bool) {
// Alert Controller 생성
let alertController = UIAlertController(title: "", message: "블록이름을 입력하세요", preferredStyle: .alert)
// TextField 추가
alertController.addTextField { textField in
textField.placeholder = "제목"
}
let addAction = UIAlertAction(title: "확인", style: .default) { [weak self, weak alertController] _ in
guard let self = self,
let alertController = alertController,
let textField = alertController.textFields?.first,
let text = textField.text, !text.isEmpty else { return }
guard let todayStr = dateLabel.text else { return }
if isFirstItem {
// 새 Item 생성 및 추가
let newItem = Item(title: text, date: todayStr)
self.addNewItem(item: newItem, replacingFirstItem: true)
} else {
// 새 Item 생성 및 추가
let newItem = Item(title: text, date: todayStr)
self.addNewItem(item: newItem)
}
}
// '취소' 액션
let cancelAction = UIAlertAction(title: "취소", style: .destructive)
alertController.addAction(cancelAction)
alertController.addAction(addAction)
// Alert 표시
DispatchQueue.main.async {
self.present(alertController, animated: true)
}
}
private func presentDeleteAlert(completionHandler: @escaping () -> Void) {
let alertController = UIAlertController(title: "", message: "정말로 삭제하시겠습니까", preferredStyle: .alert)
let deleteAction = UIAlertAction(title: "확인", style: .default) { _ in
completionHandler()
}
let cancelAction = UIAlertAction(title: "취소", style: .destructive)
alertController.addAction(cancelAction)
alertController.addAction(deleteAction)
DispatchQueue.main.async {
self.present(alertController, animated: true)
}
}
private func addNewItem(item: Item, replacingFirstItem: Bool = false) {
var newSnapshot = dataSource.snapshot()
if replacingFirstItem {
newSnapshot.deleteItems([newSnapshot.itemIdentifiers.first!])
}
newSnapshot.appendItems([item], toSection: .main)
dataSource.apply(newSnapshot, animatingDifferences: true) {
self.updateAllButtonStates()
self.saveItems()
}
}
// MARK: Data Save & Load
private func saveItems() {
print(#function)
let currentSnapshot = dataSource.snapshot()
let items = currentSnapshot.itemIdentifiers
// 아이템 저장
if let encoded = try? JSONEncoder().encode(items) {
UserDefaults.standard.set(encoded, forKey: "savedItems")
}
// 날짜별 존재 여부 저장
// 동일 날짜 중복방지를 위해 Set 사용
var datesWithItems = Set<String>()
items.forEach { datesWithItems.insert($0.date) }
UserDefaults.standard.set(Array(datesWithItems), forKey: "datesWithItems")
}
private func loadItems() {
print(#function)
if let savedItems = UserDefaults.standard.object(forKey: "savedItems") as? Data {
if let decodedItems = try? JSONDecoder().decode([Item].self, from: savedItems) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(decodedItems)
dataSource.apply(snapshot, animatingDifferences: false)
}
}
}
private func applyInitialSnapshots() {
print(#function)
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
guard let todayStr = dateLabel.text else { return }
let tutorialItem = Item(title: Item.initialMessage, date: todayStr)
snapshot.appendItems([tutorialItem])
dataSource.apply(snapshot, animatingDifferences: false)
}
}
extension MainViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// 현재 스냅샷에서 전체 아이템의 수를 계산
let itemCount = dataSource.snapshot().numberOfItems
// 각 섹션의 높이를 계산
let sectionHeight = (collectionView.bounds.height - (2 * CGFloat(itemCount) * 5)) / CGFloat(itemCount)
// 셀의 크기 반환
return CGSize(width: collectionView.bounds.width, height: sectionHeight)
}
}
extension MainViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("indexPath: \(indexPath)")
if let cell = collectionView.cellForItem(at: indexPath) as? CustomCollectionViewListCell {
}
}
}
extension MainViewController: UICollectionViewDragDelegate, UICollectionViewDropDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return [] }
let itemProvider = NSItemProvider(object: item.id.uuidString as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = item
return [dragItem]
}
func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool {
return true
// return session.canLoadObjects(ofClass: NSString.self)
}
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
return UICollectionViewDropProposal(operation: .move,
intent: .insertAtDestinationIndexPath)
}
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
guard let destinationIndexPath = coordinator.destinationIndexPath,
let item = coordinator.items.first,
let sourceIndexPath = item.sourceIndexPath else {
return
}
// 스냅샷을 가져옴
var snapshot = dataSource.snapshot()
// 해당 아이템을 식별
guard let identifier = dataSource.itemIdentifier(for: sourceIndexPath) else {
return
}
// 스냅샷에서 아이템을 삭제 및 새 위치에 삽입
snapshot.deleteItems([identifier])
if let destinationIdentifier = dataSource.itemIdentifier(for: destinationIndexPath) {
snapshot.insertItems([identifier], beforeItem: destinationIdentifier)
} else {
snapshot.appendItems([identifier], toSection: .main)
}
// 스냅샷을 적용
dataSource.apply(snapshot, animatingDifferences: true) {
self.saveItems()
}
}
}
extension MainViewController: CustomCollectionViewListCellDelegate {
func deleteItem(cell: CustomCollectionViewListCell) {
presentDeleteAlert { [weak self] in
self?.performDeletion(cell)
}
}
private func performDeletion(_ cell: CustomCollectionViewListCell) {
guard let indexPath = collectionView.indexPath(for: cell),
let item = dataSource.itemIdentifier(for: indexPath) else { return }
var snapshot = dataSource.snapshot()
if snapshot.numberOfItems == 1 {
// 마지막 아이템 삭제 시, 튜토리얼 아이템 추가 및 편집 모드 종료
snapshot.deleteItems([item])
snapshot.appendItems([Item.tutorialItem], toSection: .main)
collectionView.isEditing = false // 편집 모드 종료
editButton.title = "Edit" // 버튼 타이틀 변경
} else {
snapshot.deleteItems([item])
}
dataSource.apply(snapshot, animatingDifferences: true) {
self.updateAllButtonStates()
self.saveItems()
}
}
}
protocol CustomCollectionViewListCellDelegate: AnyObject {
func deleteItem(cell: CustomCollectionViewListCell)
}
class CustomCollectionViewListCell: UICollectionViewListCell {
static let identifier = "CustomCollectionViewListCell"
weak var delegate: CustomCollectionViewListCellDelegate?
override var isSelected: Bool {
didSet {
print(#function)
}
}
lazy var titleLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = .black
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
lazy var containerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemGray5
view.layer.cornerRadius = 30
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
// self.backgroundColor = .clear
contentView.addSubview(containerView)
containerView.addSubview(titleLabel)
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: self.contentView.topAnchor),
containerView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
containerView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 10),
containerView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -10),
titleLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
titleLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
titleLabel.text = nil // 텍스트 초기화
}
func configure(with title: String) {
titleLabel.text = title
}
override func updateConfiguration(using state: UICellConfigurationState) {
super.updateConfiguration(using: state)
// 편집 모드에 따라 액세서리 설정
if state.isEditing {
let deleteAction = UICellAccessory.delete(displayed: .whenEditing) { [weak self] in
print("deleteAction")
guard let self = self else { return }
self.delegate?.deleteItem(cell: self)
}
accessories = [deleteAction]
} else {
// 편집 모드가 아닐 때는 액세서리 제거
accessories = []
}
}
}