diff --git a/DailyQuest/DailyQuest.xcodeproj/project.pbxproj b/DailyQuest/DailyQuest.xcodeproj/project.pbxproj index 5634790..7ceeb9c 100644 --- a/DailyQuest/DailyQuest.xcodeproj/project.pbxproj +++ b/DailyQuest/DailyQuest.xcodeproj/project.pbxproj @@ -16,6 +16,12 @@ 3449AD6029222B3900B87619 /* UserInfoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3449AD5F29222B3900B87619 /* UserInfoCell.swift */; }; 349955122923220E007AB99E /* SwiftUIPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349955112923220E007AB99E /* SwiftUIPreview.swift */; }; 3499551529232533007AB99E /* UIColor+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3499551429232533007AB99E /* UIColor+.swift */; }; + 3499551E29233DEF007AB99E /* QuestCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3499551D29233DEF007AB99E /* QuestCell.swift */; }; + 3499552029234637007AB99E /* CustomProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3499551F29234637007AB99E /* CustomProgressBar.swift */; }; + 3499552329234D5F007AB99E /* BrowseCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3499552229234D5F007AB99E /* BrowseCell.swift */; }; + 3499552729235D1E007AB99E /* BrowseItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3499552629235D1E007AB99E /* BrowseItemViewModel.swift */; }; + 349955292923600A007AB99E /* BrowseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349955282923600A007AB99E /* BrowseViewController.swift */; }; + 3499552B29236041007AB99E /* BrowseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3499552A29236041007AB99E /* BrowseViewModel.swift */; }; 34ACC32D291DE9C000741371 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34ACC32C291DE9C000741371 /* AppDelegate.swift */; }; 34ACC32F291DE9C000741371 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34ACC32E291DE9C000741371 /* SceneDelegate.swift */; }; 34ACC336291DE9C100741371 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 34ACC335291DE9C100741371 /* Assets.xcassets */; }; @@ -28,8 +34,6 @@ 34ACC364291DEF6100741371 /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 34ACC363291DEF6100741371 /* FirebaseFirestore */; }; 34ACC366291DEF6100741371 /* FirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 34ACC365291DEF6100741371 /* FirebaseStorage */; }; 34ACC36C291DF0DD00741371 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 34ACC36B291DF0DD00741371 /* GoogleService-Info.plist */; }; - 9B1CFB392922AFC700CCE97A /* QuestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B1CFB362922763E00CCE97A /* QuestView.swift */; }; - 9B1CFB3B2922BBDA00CCE97A /* BrowseCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B1CFB342922460100CCE97A /* BrowseCell.swift */; }; B50078D629222F3F0070AFC4 /* CircleCheckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50078D529222F3F0070AFC4 /* CircleCheckView.swift */; }; B58DFC0A29227DA800C68A4B /* CalendarCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B58DFC0929227DA800C68A4B /* CalendarCell.swift */; }; /* End PBXBuildFile section */ @@ -57,6 +61,12 @@ 3449AD5F29222B3900B87619 /* UserInfoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfoCell.swift; sourceTree = ""; }; 349955112923220E007AB99E /* SwiftUIPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIPreview.swift; sourceTree = ""; }; 3499551429232533007AB99E /* UIColor+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+.swift"; sourceTree = ""; }; + 3499551D29233DEF007AB99E /* QuestCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestCell.swift; sourceTree = ""; }; + 3499551F29234637007AB99E /* CustomProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomProgressBar.swift; sourceTree = ""; }; + 3499552229234D5F007AB99E /* BrowseCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseCell.swift; sourceTree = ""; }; + 3499552629235D1E007AB99E /* BrowseItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseItemViewModel.swift; sourceTree = ""; }; + 349955282923600A007AB99E /* BrowseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseViewController.swift; sourceTree = ""; }; + 3499552A29236041007AB99E /* BrowseViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseViewModel.swift; sourceTree = ""; }; 34ACC329291DE9C000741371 /* DailyQuest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DailyQuest.app; sourceTree = BUILT_PRODUCTS_DIR; }; 34ACC32C291DE9C000741371 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 34ACC32E291DE9C000741371 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -69,8 +79,6 @@ 34ACC34D291DE9C100741371 /* DailyQuestUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyQuestUITests.swift; sourceTree = ""; }; 34ACC34F291DE9C100741371 /* DailyQuestUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyQuestUITestsLaunchTests.swift; sourceTree = ""; }; 34ACC36B291DF0DD00741371 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; - 9B1CFB342922460100CCE97A /* BrowseCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseCell.swift; sourceTree = ""; }; - 9B1CFB362922763E00CCE97A /* QuestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestView.swift; sourceTree = ""; }; B50078D529222F3F0070AFC4 /* CircleCheckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleCheckView.swift; sourceTree = ""; }; B58DFC0929227DA800C68A4B /* CalendarCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarCell.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -121,8 +129,8 @@ isa = PBXGroup; children = ( 3499551C292332DE007AB99E /* Cells */, - 9B1CFB362922763E00CCE97A /* QuestView.swift */, B50078D529222F3F0070AFC4 /* CircleCheckView.swift */, + 3499551F29234637007AB99E /* CustomProgressBar.swift */, ); path = Common; sourceTree = ""; @@ -175,6 +183,8 @@ 3499551A292332B2007AB99E /* Browse */ = { isa = PBXGroup; children = ( + 3499552429235034007AB99E /* View */, + 349955252923503C007AB99E /* ViewModel */, ); path = Browse; sourceTree = ""; @@ -189,13 +199,31 @@ 3499551C292332DE007AB99E /* Cells */ = { isa = PBXGroup; children = ( - 9B1CFB342922460100CCE97A /* BrowseCell.swift */, B58DFC0929227DA800C68A4B /* CalendarCell.swift */, 3449AD5F29222B3900B87619 /* UserInfoCell.swift */, + 3499551D29233DEF007AB99E /* QuestCell.swift */, + 3499552229234D5F007AB99E /* BrowseCell.swift */, ); path = Cells; sourceTree = ""; }; + 3499552429235034007AB99E /* View */ = { + isa = PBXGroup; + children = ( + 349955282923600A007AB99E /* BrowseViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 349955252923503C007AB99E /* ViewModel */ = { + isa = PBXGroup; + children = ( + 3499552629235D1E007AB99E /* BrowseItemViewModel.swift */, + 3499552A29236041007AB99E /* BrowseViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; 34ACC320291DE9C000741371 = { isa = PBXGroup; children = ( @@ -434,17 +462,21 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9B1CFB3B2922BBDA00CCE97A /* BrowseCell.swift in Sources */, + 3499552029234637007AB99E /* CustomProgressBar.swift in Sources */, + 3499552329234D5F007AB99E /* BrowseCell.swift in Sources */, + 3499551E29233DEF007AB99E /* QuestCell.swift in Sources */, 3449AD5D2922197000B87619 /* User.swift in Sources */, B50078D629222F3F0070AFC4 /* CircleCheckView.swift in Sources */, + 3499552729235D1E007AB99E /* BrowseItemViewModel.swift in Sources */, + 349955292923600A007AB99E /* BrowseViewController.swift in Sources */, B58DFC0A29227DA800C68A4B /* CalendarCell.swift in Sources */, 3499551529232533007AB99E /* UIColor+.swift in Sources */, 34ACC32D291DE9C000741371 /* AppDelegate.swift in Sources */, + 3499552B29236041007AB99E /* BrowseViewModel.swift in Sources */, 3449AD5B2922164B00B87619 /* Quest.swift in Sources */, 349955122923220E007AB99E /* SwiftUIPreview.swift in Sources */, 3449AD6029222B3900B87619 /* UserInfoCell.swift in Sources */, 34ACC32F291DE9C000741371 /* SceneDelegate.swift in Sources */, - 9B1CFB392922AFC700CCE97A /* QuestView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/DailyQuest/DailyQuest/Application/SceneDelegate.swift b/DailyQuest/DailyQuest/Application/SceneDelegate.swift index cbf11c4..15caf7e 100644 --- a/DailyQuest/DailyQuest/Application/SceneDelegate.swift +++ b/DailyQuest/DailyQuest/Application/SceneDelegate.swift @@ -16,7 +16,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // 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). - guard let _ = (scene as? UIWindowScene) else { return } + guard let windowScene = (scene as? UIWindowScene) else { return } + let cont = BrowseViewController.create(with: BrowseViewModel()) + self.window = UIWindow(windowScene: windowScene) + self.window?.rootViewController = cont + self.window?.makeKeyAndVisible() + + return } func sceneDidDisconnect(_ scene: UIScene) { diff --git a/DailyQuest/DailyQuest/Presentation/Browse/View/BrowseViewController.swift b/DailyQuest/DailyQuest/Presentation/Browse/View/BrowseViewController.swift new file mode 100644 index 0000000..aa86ae4 --- /dev/null +++ b/DailyQuest/DailyQuest/Presentation/Browse/View/BrowseViewController.swift @@ -0,0 +1,77 @@ +// +// BrowseViewController.swift +// DailyQuest +// +// Created by jinwoong Kim on 2022/11/15. +// + +import UIKit + +import RxSwift +import RxCocoa + +final class BrowseViewController: UITableViewController { + private var viewModel: BrowseViewModel! + private var disposableBag = DisposeBag() + + // MARK: - Life Cycle + static func create(with viewModel: BrowseViewModel) -> BrowseViewController { + let view = BrowseViewController() + view.viewModel = viewModel + return view + } + + override func viewDidLoad() { + super.viewDidLoad() + + configure() + bind() + } + + /** + table view의 기본 정보를 설정합니다. + */ + private func configure() { + // 셀 사이사이의 구분선을 제거합니다. + tableView.separatorStyle = .none + + tableView.allowsSelection = false + + // 델리게이트와 데이터소스를 rx로 재설정합니다. + tableView.delegate = nil + tableView.dataSource = nil + tableView.rx.setDelegate(self).disposed(by: disposableBag) + + // BrowseCell을 등록합니다. + tableView.register(BrowseCell.self, forCellReuseIdentifier: BrowseCell.reuseIdentifier) + } + + private func bind() { + viewModel + .data + .bind(to: tableView.rx.items(cellIdentifier: BrowseCell.reuseIdentifier, cellType: BrowseCell.self)) { row, item, cell in + cell.setup(with: BrowseItemViewModel(user: item.0, quests: item.1)) + } + .disposed(by: disposableBag) + } +} + +extension BrowseViewController { + /** + 하나의 BrowseCell의 크기를 결정합니다. BrowseCell내의 QuestCell의 개수만큼 크기가 늘어납니다. + +20은 margins으로 인해 추가된 값입니다. + */ + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return (75.0 * CGFloat(viewModel.cellCount[indexPath.row])) + 20 + 75 + } + + /** + viewModel에서 총 몇개의 BrowseCell을 만들어야하는지 결정합니다. + viewModel에는 UseCase를 통해 들어온 (User, [Quest]) 데이터에서 퀘스트의 **길이** 정보를 담는 프로퍼티(정ㅅ배열)가 있어야 합니다. + Note. 여기에서는 Quest 정보 자체가 필요하진 않습니다. 해당 배열의 아이템들은 위에서 사용할, BrowseCell의 크기를 결정하고, + 배열 자체의 길이는 이 메서드의 결과를 결정합니다. + */ + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return viewModel.cellCount.count + } +} diff --git a/DailyQuest/DailyQuest/Presentation/Browse/ViewModel/BrowseItemViewModel.swift b/DailyQuest/DailyQuest/Presentation/Browse/ViewModel/BrowseItemViewModel.swift new file mode 100644 index 0000000..b1d10e9 --- /dev/null +++ b/DailyQuest/DailyQuest/Presentation/Browse/ViewModel/BrowseItemViewModel.swift @@ -0,0 +1,18 @@ +// +// BrowseCellItemModel.swift +// DailyQuest +// +// Created by jinwoong Kim on 2022/11/15. +// + +import Foundation + +final class BrowseItemViewModel { + let user: User + let quests: [Quest] + + init(user: User, quests: [Quest]) { + self.user = user + self.quests = quests + } +} diff --git a/DailyQuest/DailyQuest/Presentation/Browse/ViewModel/BrowseViewModel.swift b/DailyQuest/DailyQuest/Presentation/Browse/ViewModel/BrowseViewModel.swift new file mode 100644 index 0000000..e518efd --- /dev/null +++ b/DailyQuest/DailyQuest/Presentation/Browse/ViewModel/BrowseViewModel.swift @@ -0,0 +1,39 @@ +// +// BrowseViewModel.swift +// DailyQuest +// +// Created by jinwoong Kim on 2022/11/15. +// + +import Foundation + +import RxSwift + +final class BrowseViewModel { + let user1 = User(uuid: UUID(), nickName: "jinwoong", profile: Data(), backgroundImage: Data(), description: "") + let quests1 = [ + Quest(title: "물마시기", startDay: Date(), endDay: Date(), repeat: 1, currentCount: 2, totalCount: 5), + Quest(title: "코딩하기", startDay: Date(), endDay: Date(), repeat: 1, currentCount: 0, totalCount: 10) + ] + + let user2 = User(uuid: UUID(), nickName: "someone", profile: Data(), backgroundImage: Data(), description: "") + let quests2 = [ + Quest(title: "물마시기", startDay: Date(), endDay: Date(), repeat: 1, currentCount: 4, totalCount: 5), + Quest(title: "책읽기", startDay: Date(), endDay: Date(), repeat: 1, currentCount: 9, totalCount: 20), + Quest(title: "달리기", startDay: Date(), endDay: Date(), repeat: 1, currentCount: 4, totalCount: 9), + Quest(title: "잠자기", startDay: Date(), endDay: Date(), repeat: 1, currentCount: 1, totalCount: 1) + ] + + let data: Observable<[(User, [Quest])]> + + let cellCount = [2, 4] + + init() { + self.data = .just([(user1, quests1), (user2, quests2)]) + } +} + +/** + Usecase + - fetching quests, it contains user and his quests. + */ diff --git a/DailyQuest/DailyQuest/Presentation/Common/Cells/BrowseCell.swift b/DailyQuest/DailyQuest/Presentation/Common/Cells/BrowseCell.swift index aaa385c..ffed93d 100644 --- a/DailyQuest/DailyQuest/Presentation/Common/Cells/BrowseCell.swift +++ b/DailyQuest/DailyQuest/Presentation/Common/Cells/BrowseCell.swift @@ -1,146 +1,121 @@ // -// QuestCell.swift +// BrowseCell.swift // DailyQuest // -// Created by 이다연 on 2022/11/14. +// Created by jinwoong Kim on 2022/11/15. // import UIKit + import SnapKit -class BrowseCell: UITableViewCell { +final class BrowseCell: UITableViewCell { + var viewModel: BrowseItemViewModel! - static let reuseIdentifier = "QuestCell" + /// dequeuResusable을 위한 아이덴티파이어입니다. + static let reuseIdentifier = "UserInfoCell" - /* - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - // Configure the view for the selected state - } + // MARK: - Components + /** + 아직 사용되고 있는 view는 아닙니다. */ - - private lazy var userStackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.alignment = .center - stackView.backgroundColor = .lightGray - stackView.spacing = 10 - stackView.isLayoutMarginsRelativeArrangement = true - stackView.layoutMargins = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) - stackView.layer.cornerRadius = 15 - return stackView - }() - - private lazy var questStackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - stackView.backgroundColor = .lightGray - stackView.spacing = 10 - stackView.distribution = .fillEqually - stackView.layer.cornerRadius = 15 - return stackView - }() - - private lazy var userImageView: UIImageView = { - let userImage = UIImageView() - userImage.image = UIImage(systemName: "heart.fill") - userImage.clipsToBounds = true - userImage.backgroundColor = .white - userImage.layer.masksToBounds = true - return userImage + private lazy var header: UILabel = { + let header = UILabel() + header.text = "test" + + return header }() - private lazy var userQuestLabel: UILabel = { - let userQuest = UILabel() - userQuest.frame = CGRect(x: 0, y: 0, width: 60, height: 25) - userQuest.font = UIFont.boldSystemFont(ofSize: 16) - userQuest.text = " " - return userQuest + private lazy var questTableView: UITableView = { + let questTableView = UITableView() + questTableView.backgroundColor = .maxLightGrey + questTableView.separatorStyle = .none + + return questTableView }() - - // MARK: - Methods override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) + + questTableView.register(QuestCell.self, forCellReuseIdentifier: QuestCell.reuseIdentifier) + questTableView.delegate = self + questTableView.dataSource = self + + questTableView.rowHeight = 75 // the cell size + + questTableView.allowsSelection = false + questTableView.sectionHeaderTopPadding = 0 + configureUI() } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - + /** + Browse Cell 내부의 테이블뷰의 모든 방향에 패딩을 추가합니다. + */ override func layoutSubviews() { super.layoutSubviews() - userImageView.layer.cornerRadius = userImageView.frame.height / 2 + + questTableView.frame = questTableView.frame.inset(by: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } private func configureUI() { - userStackView.addArrangedSubview(userImageView) - userStackView.addArrangedSubview(userQuestLabel) - questStackView.addArrangedSubview(userStackView) - - addSubview(questStackView) + addSubview(questTableView) - userStackView.snp.makeConstraints { make in - make.width.equalTo(questStackView).inset(10) - make.height.equalTo(80) - } - - questStackView.snp.makeConstraints { make in - make.left.equalToSuperview().offset(20) - make.right.equalToSuperview().offset(-20) - } - - - userImageView.snp.makeConstraints { make in - make.height.equalTo(userStackView.snp.height).offset(-50) - make.width.equalTo(userImageView.snp.height) - } - - for subview in questStackView.subviews { - subview.snp.makeConstraints { (make) in - make.height.equalTo(subview.frame.height) - make.width.equalTo(questStackView.snp.width) - } + questTableView.snp.makeConstraints { make in + make.edges.equalToSuperview() } } - func setup(user: User, quest: [Quest]) { - userQuestLabel.text = user.nickName + "님의 Today Quest" - //guard let image = UIImage(data: user.profile) else { return } - //userImageView.image = image - - let height:Double = 100.0 * (1.0 + Double(quest.count)) - - print(height) - - questStackView.snp.makeConstraints { make in - make.height.equalTo(height) - } - - for q in quest { - var questView = QuestView() - questView.setUp(with: q) - questStackView.addArrangedSubview(questView) - } - + /** + 인자로 viewModel을 받아, 테이블뷰를 reload합니다. + + - Parameters: + - viewModel: `BrowseItemViewModel` 타입입니다. User의 인스턴스와 Quest 인스턴스의 배열을 가지고 있습니다. + */ + func setup(with viewModel: BrowseItemViewModel) { + self.viewModel = viewModel + questTableView.reloadData() } } -#if canImport(SwiftUI) && DEBUG -import SwiftUI +extension BrowseCell: UITableViewDelegate { + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return header + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return 75 + } +} -struct BrowseCellPreview: PreviewProvider{ - static var previews: some View { - UIViewPreview { - let cell = BrowseCell(frame: .zero) - cell.setup(user: User(uuid: UUID(), nickName: "맥스", profile: Data(), backgroundImage: Data(), description: ""), quest: [Quest(title: "물 마시기", startDay: Date(), endDay: Date(), repeat: 0, currentCount: 1, totalCount: 5), Quest(title: "물 마시기", startDay: Date(), endDay: Date(), repeat: 0, currentCount: 1, totalCount: 5), Quest(title: "물 마시기", startDay: Date(), endDay: Date(), repeat: 0, currentCount: 1, totalCount: 5)]) - return cell +extension BrowseCell: UITableViewDataSource { + /** + 테이블 뷰안에 들어갈 QuestCell의 개수를 구합니다. + Note. 이 메서드가 최초로 실행되는 시점에는 viewModel이 nil입니다. + 데이터소스를 통해 값이 삽입되는 시점에는 그렇지 않으므로, 예외처리를 통해 해결했습니다. + */ + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + guard let count = viewModel?.quests.count else { return 0 } + return count + } + + /** + QuestCell을 생성합니다. + */ + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = questTableView.dequeueReusableCell(withIdentifier: QuestCell.reuseIdentifier, for: indexPath) as? QuestCell else { + assertionFailure("Cannot deque reuseable cell.") + return UITableViewCell() } - .previewLayout(.sizeThatFits) + + cell.setup(with: viewModel.quests[indexPath.row]) + + return cell } } -#endif diff --git a/DailyQuest/DailyQuest/Presentation/Common/Cells/QuestCell.swift b/DailyQuest/DailyQuest/Presentation/Common/Cells/QuestCell.swift new file mode 100644 index 0000000..38433d6 --- /dev/null +++ b/DailyQuest/DailyQuest/Presentation/Common/Cells/QuestCell.swift @@ -0,0 +1,122 @@ +// +// QuestCell.swift +// DailyQuest +// +// Created by jinwoong Kim on 2022/11/15. +// + +import UIKit + +import SnapKit + +final class QuestCell: UITableViewCell { + /// dequeuResusable을 위한 아이덴티파이어입니다. + static let reuseIdentifier = "UserInfoCell" + + // MARK: - Components + /** + 프로그래스 바 입니다. 기존의 `UIProgressBar`를 상속받는 + `CustomProgressBar`를 타입으로 합니다. + 이 클래스는 코너의 radius 값을 주기 위해 만들어졌습니다. + */ + private lazy var progressView: CustomProgressBar = { + let progressView = CustomProgressBar() + progressView.trackTintColor = .maxLightYellow + progressView.progressTintColor = .maxYellow + progressView.progress = 0.2 + + return progressView + }() + + /** + 좌측에 quest의 제목이 들어갈 레이블입니다. + */ + private lazy var questLabel: UILabel = { + let questLabel = UILabel() + questLabel.text = "0" + questLabel.font = UIFont.boldSystemFont(ofSize: 16) + return questLabel + }() + + /** + 우측에 현재 달성량과 목표량이 들어갈 레이블입니다. + */ + private lazy var countLabel: UILabel = { + let countLabel = UILabel() + countLabel.text = "0" + countLabel.font = UIFont.boldSystemFont(ofSize: 16) + return countLabel + }() + + + // MARK: - Methods + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + configureUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /** + UI의 constraints를 설정하기 위한 메서드입니다. + constraints를 설정하기 전에, 해당 뷰를 먼저 add해야함을 유념하세요. + */ + private func configureUI() { + addSubview(progressView) + addSubview(questLabel) + addSubview(countLabel) + + backgroundColor = .maxLightGrey + + progressView.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(10) + } + + questLabel.snp.makeConstraints { make in + make.leading.equalToSuperview().inset(20) + make.centerY.equalToSuperview() + } + + countLabel.snp.makeConstraints { make in + make.trailing.equalTo(progressView).inset(20) + make.centerY.equalToSuperview() + } + } + + /** + 인자로 받은 Entity Quest타입을 통해 그 정보를 기반으로 cell에 아이템을 넣습니다. + + 애니메이션 효과가 필요없다고 판단하여, `setProgress(_:animated)`에서 두번째 인자를 + `false`로 설정하였습니다. + + - Parameters: + - quest: Quest타입의 엔티티입니다. + */ + func setup(with quest: Quest) { + let value = Float(quest.currentCount) / Float(quest.totalCount) + questLabel.text = "\(quest.title)" + countLabel.text = "\(quest.currentCount) / \(quest.totalCount)" + + progressView.setProgress(value, animated: false) + } +} + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct QuestCellPreview: PreviewProvider{ + static var previews: some View { + UIViewPreview { + let cell = QuestCell(frame: .zero) + let quest = Quest(title: "my quest", startDay: Date(), endDay: Date(), repeat: 1, currentCount: 2, totalCount: 5) + + cell.setup(with: quest) + return cell + }.previewLayout(.fixed(width: 300, height: 80)) + } +} +#endif + diff --git a/DailyQuest/DailyQuest/Presentation/Common/CustomProgressBar.swift b/DailyQuest/DailyQuest/Presentation/Common/CustomProgressBar.swift new file mode 100644 index 0000000..4b77e93 --- /dev/null +++ b/DailyQuest/DailyQuest/Presentation/Common/CustomProgressBar.swift @@ -0,0 +1,19 @@ +// +// CustomProgressBar.swift +// DailyQuest +// +// Created by jinwoong Kim on 2022/11/15. +// + +import UIKit + +final class CustomProgressBar: UIProgressView { + + override func layoutSubviews() { + super.layoutSubviews() + subviews.forEach { subview in + subview.layer.masksToBounds = true + subview.layer.cornerRadius = 15 + } + } +} diff --git a/DailyQuest/DailyQuest/Presentation/Common/QuestView.swift b/DailyQuest/DailyQuest/Presentation/Common/QuestView.swift deleted file mode 100644 index 0776e06..0000000 --- a/DailyQuest/DailyQuest/Presentation/Common/QuestView.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// QuestView.swift -// DailyQuest -// -// Created by 이다연 on 2022/11/14. -// - -import UIKit -import SnapKit - -class QuestView: UIView { - - // assets 에 한번에 나중에 추가하려고 일단은 임의로 색상 넣음 - private var backgroundLabel: UILabel = { - let label = UILabel() - label.frame = CGRect(x: 0, y: 0, width: 300, height: 80) - label.backgroundColor = .yellow - label.layer.masksToBounds = true - return label - }() - - private var progressLabel: UILabel = { - let label = UILabel() - label.frame = CGRect(x: 0, y: 0, width: 0, height: 80) - label.backgroundColor = .systemYellow - label.layer.masksToBounds = true - return label - }() - - private var questLabel: UILabel = { - let label = UILabel() - label.frame = CGRect(x: 0, y: 0, width: 60, height: 25) - label.textAlignment = .center - label.text = "" - label.font = UIFont.boldSystemFont(ofSize: 16) - label.sizeToFit() - return label - }() - - private var countLabel: UILabel = { - let label = UILabel() - label.frame = CGRect(x: 0, y: 0, width: 60, height: 25) - label.textAlignment = .center - label.text = "" - label.font = UIFont.boldSystemFont(ofSize: 16) - label.sizeToFit() - return label - }() - - override init(frame: CGRect) { - super.init(frame: frame) - configureUI() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - backgroundLabel.layer.cornerRadius = backgroundLabel.bounds.size.height / 7 - progressLabel.layer.cornerRadius = progressLabel.bounds.size.height / 8 - } - - private func configureUI() { - self.addSubview(backgroundLabel) - self.addSubview(progressLabel) - self.addSubview(questLabel) - self.addSubview(countLabel) - - backgroundLabel.snp.makeConstraints { make in - make.height.equalTo(80) - make.width.equalTo(300) - } - - progressLabel.snp.makeConstraints { make in - make.height.equalTo(80) - make.leading.equalTo(backgroundLabel) - } - - questLabel.snp.makeConstraints { make in - make.top.equalTo(backgroundLabel).inset(30) - make.left.equalTo(backgroundLabel).inset(10) - } - - countLabel.snp.makeConstraints { make in - make.top.equalTo(backgroundLabel).inset(30) - make.right.equalTo(backgroundLabel).inset(10) - } - - } - - func setUp(with quest: Quest) { - let progressWidth = 300.0 * (Double(quest.currentCount)/Double(quest.totalCount)) - - questLabel.text = quest.title - countLabel.text = String(quest.currentCount) + " / " + String(quest.totalCount) - progressLabel.snp.makeConstraints { make in - make.width.equalTo(progressWidth) - } - } -} - -#if canImport(SwiftUI) && DEBUG -import SwiftUI - -struct QuestionViewPreview: PreviewProvider{ - static var previews: some View { - UIViewPreview { - let view = QuestView(frame: .zero) - view.setUp(with: Quest(title: "물 마시기", startDay: Date(), endDay: Date(), repeat: 0, currentCount: 2, totalCount: 5)) - return view - }.previewLayout((.fixed(width: 300, height: 80))) - } -} -#endif -