*# ๐Ÿ—“ ํ”„๋กœ์ ํŠธ ๋งค๋‹ˆ์ € ## ๐Ÿ“š ๋ชฉ์ฐจ - [์†Œ๊ฐœ](#-์†Œ๊ฐœ) - [ํƒ€์ž„๋ผ์ธ](#-ํƒ€์ž„๋ผ์ธ) - [ํŒŒ์ผ๊ตฌ์กฐ](#-ํŒŒ์ผ๊ตฌ์กฐ) - [์‹คํ–‰ํ™”๋ฉด](#-์‹คํ–‰ํ™”๋ฉด) - [ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…](#-ํŠธ๋Ÿฌ๋ธ”-์ŠˆํŒ…) - [์ฐธ๊ณ  ๋งํฌ](#์ฐธ๊ณ -๋งํฌ) ## ๐Ÿ—ฃ ์†Œ๊ฐœ ### ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ #### ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„ : 23.05.15 ~ 23.06.02 - ToDo๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ํ”„๋กœ๊ทธ๋žจ ### ํŒ€ ์†Œ๊ฐœ - ํŒ€ ๊ตฌ์„ฑ์›(2์ธ) | vetto | ์†ก์ค€ | | :--------: | :--------: | | <img src="https://cdn.discordapp.com/attachments/535779947118329866/1055718870951940146/1671110054020-0.jpg" width="200" height="250"/> | <img src="https://user-images.githubusercontent.com/88870642/210026753-591175fe-27c1-4335-a2cb-f883bfeb2784.png" width="200" height="200"/>| |[Github](https://github.com/gzzjk159)|[Github](https://github.com/kimseongj)| ## โฑ ํƒ€์ž„๋ผ์ธ |๋‚ ์งœ|ํ™œ๋™| |---|---| |2023.05.16|- ๊ธฐ์ˆ  ์„ ์ •| |2023.05.17|- CompositionalLayout๊ณผ DiffableDataSource๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ CollectionView ๊ตฌํ˜„| |2023.05.18|- CollectionView์™€ TableView์— ๋Œ€ํ•œ ๋น„๊ต ๋ฐ ๊ตฌํ˜„| |2023.05.19|- CollectionView Swipe ๊ตฌํ˜„| |2023.05.22|- ModalView ๊ตฌํ˜„ </br> - DateFormmatter ์„ค์ •| |2023.05.23|- ViewModel ๊ตฌํ˜„ </br> - MVVM Bind ๊ตฌํ˜„| |2023.05.24|- ViewModel ๋ฆฌํŒฉํ† ๋ง </br> - DatePicker ๊ตฌํ˜„| |2023.05.25|- LongPressGesture ๊ตฌํ˜„ </br> Popover ๊ตฌํ˜„| |2023.05.26|- ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง| |2023.05.29|- Combine์„ ํ†ตํ•œ Binding| |2023.05.30|- Constant๊ฐ’ ์ƒ์ˆ˜ ์„ ์–ธ ๋ฐ firebase ์‹คํ—˜| |2023.05.31|- ViewModel ๋ฐ์ดํ„ฐ๋ฅผ ScheduleManager๋กœ ๋ถ„๋ฆฌ| |2023.06.01|- MVVM ๋ฆฌํŒฉํ† ๋ง| |2023.06.02|- Readme ์ž‘์„ฑ| ## ๐Ÿ“‚ ํŒŒ์ผ๊ตฌ์กฐ ```swift . โ”œโ”€โ”€ Resource โ”‚ โ”œโ”€โ”€ Assets.xcassets โ”‚ โ”‚ โ”œโ”€โ”€ AccentColor.colorset โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ Contents.json โ”‚ โ”‚ โ”œโ”€โ”€ AppIcon.appiconset โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ Contents.json โ”‚ โ”‚ โ””โ”€โ”€ Contents.json โ”‚ โ”œโ”€โ”€ Base.lproj โ”‚ โ”‚ โ””โ”€โ”€ LaunchScreen.storyboard โ”‚ โ”œโ”€โ”€ GoogleService-Info.plist โ”‚ โ””โ”€โ”€ Info.plist โ””โ”€โ”€ Source โ”œโ”€โ”€ Application โ”‚ โ”œโ”€โ”€ AppDelegate.swift โ”‚ โ””โ”€โ”€ SceneDelegate.swift โ”œโ”€โ”€ Model โ”‚ โ”œโ”€โ”€ ModalType.swift โ”‚ โ”œโ”€โ”€ Schedule.swift โ”‚ โ”œโ”€โ”€ ScheduleManager.swift โ”‚ โ””โ”€โ”€ ScheduleType.swift โ”œโ”€โ”€ Protocol โ”‚ โ””โ”€โ”€ IdentifierType.swift โ”œโ”€โ”€ Utility โ”‚ โ”œโ”€โ”€ DateFormatterManager.swift โ”‚ โ”œโ”€โ”€ FirebaseManager.swift โ”‚ โ”œโ”€โ”€ Namespace.swift โ”‚ โ””โ”€โ”€ Section.swift โ”œโ”€โ”€ View โ”‚ โ”œโ”€โ”€ DoList โ”‚ โ”‚ โ”œโ”€โ”€ DoListViewController.swift โ”‚ โ”‚ โ”œโ”€โ”€ ScheduleCell.swift โ”‚ โ”‚ โ””โ”€โ”€ TodoHeaderView.swift โ”‚ โ”œโ”€โ”€ Main โ”‚ โ”‚ โ””โ”€โ”€ MainViewController.swift โ”‚ โ””โ”€โ”€ Modal โ”‚ โ””โ”€โ”€ ModalViewController.swift โ””โ”€โ”€ ViewModel โ”œโ”€โ”€ DoListViewModel.swift โ””โ”€โ”€ ModalViewModel.swift ``` ## ๐Ÿ’ป ์‹คํ–‰ํ™”๋ฉด | schedule ์ƒ์„ฑ | Schedule ์ด๋™| | :--------: | :--------: | | ![์Šค์ผ€์ฅด์ถ”๊ฐ€](https://github.com/kimseongj/TIL/assets/88870642/d4441bed-0974-4646-ac53-1cbb8cd41ad5)| ![์Šค์ผ€์ฅด์ด๋™](https://github.com/kimseongj/TIL/assets/88870642/61a133d5-a03e-49e4-8553-9dd08982f820)| | schedule ์‚ญ์ œ | schedule ์ˆ˜์ • | | :---: | :---: | |![์Šค์ผ€์ฅด์‚ญ์ œ](https://github.com/kimseongj/TIL/assets/88870642/6cb6e98e-f5f5-4dd2-b59a-1290136436bb)|![์Šค์ผ€์ฅด๋ณ€๊ฒฝ](https://github.com/kimseongj/TIL/assets/88870642/f98d8264-0420-43ca-880f-0dab59f8dd53)| ## ๐Ÿ”ฅ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ… ### 1๏ธโƒฃCombine @Published์˜ willset ![](https://hackmd.io/uploads/SkehwApSh.png) > `Published`์˜ ๊ฒฝ์šฐ ๊ณต์‹๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•œ ๊ฒฐ๊ณผ, subscriber๋Š” ์‹ค์ œ๋กœ ์†์„ฑ์— ์„ค์ •๋˜๊ธฐ ์ „์— ์ƒˆ ๊ฐ’์„ ๋ฐ›์Šต๋‹ˆ๋‹ค. ์ด ์ ์„ ๊ฐ„๊ณผํ•˜๊ณ , ์‹ค์ œ ์†์„ฑ์— ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐ”๋žŒ์— ํ•œ๋ฐ•์ž ๋Šฆ๊ฒŒ ์ž‘๋™๋˜๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” ๊ทธ์— ๋Œ€ํ•œ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค. ์ฆ‰, `@Published`๋กœ ๊ฐ์‹ธ์ ธ์žˆ๋Š” ํ”„๋กœํผํ‹ฐ๋Š” `sink`ํ•  ๊ฒฝ์šฐ `willset`์ด ์ž‘๋™๋˜์–ด, `weather.temperature`๋ฅผ `sink`์˜ `escaping closure`์— ๋ถˆ๋Ÿฌ์˜ฌ ๊ฒฝ์šฐ ๋ณ€ํ•˜์ง€ ์•Š์€ ์ƒํƒœ๋กœ ๊ฐ’์ด ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค. `sink`์˜ `escaping closure` ๋‚ด๋ถ€์— ์žˆ๋Š” ๋งค๊ฐœ๋ณ€์ˆ˜($0)์— ๋ณ€ํ™”๋œ ๊ฐ’์ด ์ €์žฅ๋˜๋ฉฐ, ์ด ๊ฐ’์„ ํ†ตํ•ด ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. - ๋ณ€๊ฒฝ๋œ ๊ฐ’์„ subscriber๊ฐ€ ๋ฐ›์•„ ์™”์„ ์‹œ ```Swift class Weather { @Published var temperature: [Double] init(temperature: [Double]) { self.temperature = temperature } } var cancellables: Set<AnyCancellable> = [] let weather = Weather(temperature: [20]) weather.$temperature .sink() { print ("Temperature now: \(weather.temperature)") }.store(in: &cancellables) weather.temperature = [25] // [20]์„ ํ˜ธ์ถœํ•œ๋‹ค. ``` - ์‹ค์ œ ์†์„ฑ ๊ฐ’์„ ๋ฐ›์•„์™€ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜์˜€์„ ์‹œ ```Swift class Weather { @Published var temperature: [Double] init(temperature: [Double]) { self.temperature = temperature } } var cancellables: Set<AnyCancellable> = [] let weather = Weather(temperature: [20]) weather.$temperature .sink() { print ("Temperature now: \($0)") }.store(in: &cancellables) weather.temperature = [25] // [20, 25]๋ฅผ printํ•œ๋‹ค. ``` ### 2๏ธโƒฃcell identifier cell์„ collectionView์— ๋“ฑ๋กํ•  ๋•Œ, "cell"์ด๋ผ๋Š” ๋‹ค์†Œ ๋ชจํ˜ธํ•œ identifier๋ฅผ ์ž‘์„ฑํ–ˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋ชจํ˜ธ์„ฑ์„ ์—†์• ๊ธฐ ์œ„ํ•ด `identifierType`์ด๋ผ๋Š” ํ”„๋กœํ† ์ฝœ์„ ๊ตฌํ˜„ํ•˜์—ฌ, `UIViewController`์™€ `UICollectionViewCell`์— ์ฑ„ํƒ์„ ํ•˜์˜€์Šต๋‹ˆ๋‹ค. - ๋ณ€๊ฒฝ ์ „ ```swift self.collectionView.register(ScheduleCell.self, forCellWithReuseIdentifier: "cell") ``` - ๋ณ€๊ฒฝ ํ›„ ```swift public protocol IdentifierType { static var identifier: String { get } } extension IdentifierType { public static var identifier: String { return String(describing: self) } } extension UICollectionViewCell: IdentifierType {} extension UIViewController: IdentifierType {} collectionView.register(ScheduleCell.self, forCellWithReuseIdentifier: ScheduleCell.identifier) ``` ### 3๏ธโƒฃ๋งค์ง ๋„˜๋ฒ„ ๊ธฐ์กด์—๋Š” constant๊ฐ™์ด CGFloat๊ฐ€ ๋“ค์–ด๊ฐ€๋Š” ์ž๋ฆฌ์— 20, 25๋“ฑ์„ ์ง์ ‘ ์ ์–ด์ฃผ๋ฉฐ ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋ ‡๊ฒŒ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ฒŒ ๋˜๋ฉด 20, 25๊ฐ™์€ ๋งค์ง ๋„˜๋ฒ„๊ฐ€ ์ƒ๊ฒจ๋ฒ„๋ฆฐ๋‹ค. ๋งค์ง ๋„˜๋ฒ„๋ฅผ ํ•ด๊ฒฐํ•ด์ฃผ๊ธฐ ์œ„ํ•ด ํŒŒ์ผ ๋‚ด๋ถ€์— private let ์œผ๋กœ ๋ณ€์ˆ˜๋ฅผ ์„ ์–ธํ•˜๊ณ  ์„ ์–ธํ•œ ๋ณ€์ˆ˜๋ฅผ ๋„ฃ์–ด์ฃผ๋Š” ์‹์œผ๋กœ ๋ฆฌํŒฉํ† ๋ง์„ ํ•˜์˜€์Šต๋‹ˆ๋‹ค. - ์ˆ˜์ • ์ „ ```swift private func configureUI() { contentView.backgroundColor = .white contentView.addSubview(stackView) stackView.addArrangedSubview(titleLabel) stackView.addArrangedSubview(contentLabel) stackView.addArrangedSubview(expirationLabel) NSLayoutConstraint.activate([ stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15), stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15), stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15), stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -15) ]) } ``` - ์ˆ˜์ • ํ›„ ```swift private let cellPadding: CGFloat = 15 ... private func configureUI() { contentView.backgroundColor = .white contentView.addSubview(stackView) stackView.addArrangedSubview(titleLabel) stackView.addArrangedSubview(contentLabel) stackView.addArrangedSubview(expirationLabel) NSLayoutConstraint.activate([ stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: cellPadding), stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -cellPadding), stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: cellPadding), stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -cellPadding) ]) } ``` ### 4๏ธโƒฃMVVM ๋ฆฌํŒฉํ† ๋ง MVVM์— ๋Œ€ํ•œ ์ •ํ™•ํ•œ ์ดํ•ด์™€ ์‚ฌ์šฉ์„ ์œ„ํ•ด ์ฝ”๋“œ๋ฅผ ๋ฆฌํŒฉํ† ๋งํ–ˆ์Šต๋‹ˆ๋‹ค. MVVM์˜ ๊ฒฝ์šฐ MVC์—์„œ ViewController์˜ ์—ญํ• ์ด ๋น„๋Œ€ํ•ด์ง์„ ๋ฐฉ์ง€ํ•˜๊ณ , ๋…๋ฆฝ์ ์ธ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ViewModel์ด ์—ฌ๋Ÿฌ ViewController์— ์‚ฌ์šฉ๋˜๊ณ , ๊ธฐ๋Šฅ์ด ๋งŽ์•„์ง„๋‹ค๋ฉด, ViewModel ๋˜ํ•œ MVC์˜ ViewController์ฒ˜๋Ÿผ ๋น„๋Œ€ํ•ด์งˆ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค. ์ด์— ๋‚ด๋ฆฐ ๊ฒฐ๋ก ์€ Viewcontroller ํ•˜๋‚˜๋‹น ํ•˜๋‚˜์˜ ViewModel์„ ๊ฐ€์ง€๊ณ  ์žˆ์–ด์•ผ ํ•˜๋ฉฐ, ViewModel์€ ์ž์‹ ์ด ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ Model์—์„œ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. - Model ```swift class ScheduleManager { static let shared = ScheduleManager() @Published var todoSchedules: [Schedule] = [] @Published var doingSchedules: [Schedule] = [] @Published var doneSchedules: [Schedule] = [] func addTodoSchedule( schedule: Schedule) { todoSchedules.append(schedule) } func sendSchedule(scheduleType: ScheduleType) -> AnyPublisher<[Schedule], Never> { switch scheduleType { case .todo: return $todoSchedules.eraseToAnyPublisher() case .doing: return $doingSchedules.eraseToAnyPublisher() case .done: return $doneSchedules.eraseToAnyPublisher() } } ... } ``` - ViewModel ```swift final class DoListViewModel { let scheduleManager = ScheduleManager.shared @Published var scheduleList: [Schedule] = [] private var cancelBag: Set<AnyCancellable> = [] init(scheduleType: ScheduleType) { fetchSchedule(scheduleType: scheduleType) } private func fetchSchedule(scheduleType: ScheduleType) { scheduleManager.sendSchedule(scheduleType: scheduleType).assign(to: \.scheduleList, on: self).store(in: &cancelBag) } func deleteSchedule(scheduleType: ScheduleType, index: Int) { scheduleManager.deleteSchedule(scheduleType: scheduleType, index: index) } func move(fromIndex: Int, from: ScheduleType, to: ScheduleType) { scheduleManager.move(fromIndex: fromIndex, from: from, to: to) } } ``` ## ์ฐธ๊ณ  ๋งํฌ - [Swift Tutorial: An Introduction to the MVVM Design Pattern](https://adevait.com/ios/swift-tutorial-mvvm-design-pattern) - [Swift Document UICollectionViewCompositionalLayout](https://developer.apple.com/documentation/uikit/uicollectionviewcompositionallayout) - [Swift Document Published](https://developer.apple.com/documentation/combine/published) - [Swift Document DatePicker](https://developer.apple.com/documentation/swiftui/datepicker) - [Swift Document Combine](https://developer.apple.com/documentation/combine)