diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..071c739eb Binary files /dev/null and b/.DS_Store differ diff --git a/iOS/issue-tracker/.swiftlint.yml b/iOS/issue-tracker/.swiftlint.yml new file mode 100644 index 000000000..d3f374f01 --- /dev/null +++ b/iOS/issue-tracker/.swiftlint.yml @@ -0,0 +1,8 @@ +disabled_rules: +- line_length +- identifier_name +included: +excluded: +- issue-trackerTests +- issue-trackerUITests + diff --git a/iOS/issue-tracker/LabelsCollectionViewCell.swift b/iOS/issue-tracker/LabelsCollectionViewCell.swift index 3540c0208..a09f246a1 100644 --- a/iOS/issue-tracker/LabelsCollectionViewCell.swift +++ b/iOS/issue-tracker/LabelsCollectionViewCell.swift @@ -8,38 +8,39 @@ import UIKit import SnapKit -class LabelsCollectionViewCell: UICollectionViewCell { +final class LabelsCollectionViewCell: UICollectionViewCell { static var identifiers = "LabelsCollectionViewCell" - - let label: PaddingLabel = { + + private let label: PaddingLabel = { var label = PaddingLabel(withInsets: 0, 0, 10, 10) label.textAlignment = .center label.textColor = .white label.layer.masksToBounds = true - label.layer.cornerRadius = 15 + label.layer.cornerRadius = 10 return label }() - + override init(frame: CGRect) { super.init(frame: frame) addSubview(label) setupAutolayout() } - + required init?(coder: NSCoder) { super.init(coder: coder) addSubview(label) setupAutolayout() } - - func setupAutolayout() { + + private func setupAutolayout() { label.snp.makeConstraints { label in label.edges.equalToSuperview() } } - + func configure(title: String, color: String) { label.text = title label.backgroundColor = UIColor.hexStringToUIColor(hex: color) + label.sizeToFit() } } diff --git a/iOS/issue-tracker/issue-tracker.xcodeproj/project.pbxproj b/iOS/issue-tracker/issue-tracker.xcodeproj/project.pbxproj index 56c177e9c..c1c488c64 100644 --- a/iOS/issue-tracker/issue-tracker.xcodeproj/project.pbxproj +++ b/iOS/issue-tracker/issue-tracker.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 491F08942679E5420081C5C5 /* AddLabelViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491F08932679E5410081C5C5 /* AddLabelViewController.swift */; }; 49903DAB266F558300D2A6DD /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49903DAA266F558300D2A6DD /* LoginView.swift */; }; 49903DAE266F588B00D2A6DD /* IDPasswordTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49903DAD266F588B00D2A6DD /* IDPasswordTextField.swift */; }; 8345AD7BD9CAFAD9A4F8374A /* Pods_issue_tracker_issue_trackerUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0166F39C919837292E32AEF /* Pods_issue_tracker_issue_trackerUITests.framework */; }; @@ -21,6 +22,13 @@ B32FEACE267260E400BF37A1 /* AddIssueButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32FEACD267260E400BF37A1 /* AddIssueButton.swift */; }; B349997D266F8D0B0091A44A /* GitHubLoginButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B349997C266F8D0B0091A44A /* GitHubLoginButton.swift */; }; B349997F266F90710091A44A /* AppleLoginButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B349997E266F90710091A44A /* AppleLoginButton.swift */; }; + B34FF73E267AE9D8002D9C56 /* EstimatedLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34FF73D267AE9D8002D9C56 /* EstimatedLabelView.swift */; }; + B34FF741267B8800002D9C56 /* AddLabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34FF740267B8800002D9C56 /* AddLabelTableViewCell.swift */; }; + B366CA41268319AC00C97190 /* LeftAlignedCollectionViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B366CA40268319AC00C97190 /* LeftAlignedCollectionViewFlowLayout.swift */; }; + B366CA43268370BA00C97190 /* CustomAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B366CA42268370BA00C97190 /* CustomAlertView.swift */; }; + B3926D6B2681B59800F72CEE /* PatchIssue.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3926D6A2681B59800F72CEE /* PatchIssue.swift */; }; + B3A5CB58267840290060DC85 /* LabelListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A5CB57267840290060DC85 /* LabelListViewModel.swift */; }; + B3A5CB5B26784AA50060DC85 /* LabelList.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A5CB5A26784AA50060DC85 /* LabelList.swift */; }; B3B559E1266E095E00901C55 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B559E0266E095E00901C55 /* AppDelegate.swift */; }; B3B559E3266E095E00901C55 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B559E2266E095E00901C55 /* SceneDelegate.swift */; }; B3B559E8266E095E00901C55 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B3B559E6266E095E00901C55 /* Main.storyboard */; }; @@ -28,8 +36,12 @@ B3B559ED266E096000901C55 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B3B559EB266E096000901C55 /* LaunchScreen.storyboard */; }; B3B559F8266E096000901C55 /* issue_trackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B559F7266E096000901C55 /* issue_trackerTests.swift */; }; B3B55A03266E096000901C55 /* issue_trackerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B55A02266E096000901C55 /* issue_trackerUITests.swift */; }; + B3BA329A2680686E0009FE72 /* IssueListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BA32992680686E0009FE72 /* IssueListViewModel.swift */; }; + B3CBD19B267C359500F8D733 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = B3CBD19A267C359500F8D733 /* .swiftlint.yml */; }; B3D7D0C4267735CC000F02F4 /* IssueList.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D7D0C3267735CC000F02F4 /* IssueList.swift */; }; B3D7D0C926773AEA000F02F4 /* NewIssue.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D7D0C826773AEA000F02F4 /* NewIssue.swift */; }; + B3E01193267A0B44001155D4 /* AddLabelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E01192267A0B44001155D4 /* AddLabelViewModel.swift */; }; + B3E01196267A28B2001155D4 /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = B3E01195267A28B2001155D4 /* MarkdownKit */; }; B3EADABE2675EFED0007C4B6 /* AddLabelButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EADABD2675EFED0007C4B6 /* AddLabelButton.swift */; }; B3EADAC0267628FB0007C4B6 /* LabelsCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EADABF267628FB0007C4B6 /* LabelsCollectionView.swift */; }; B3EADAC2267629190007C4B6 /* LabelsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EADAC1267629190007C4B6 /* LabelsCollectionViewCell.swift */; }; @@ -38,15 +50,17 @@ B3F5275C2670C0EF002B0812 /* PaddingLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F5275B2670C0EF002B0812 /* PaddingLabel.swift */; }; B3FBA7B726730B9A0006E5E6 /* IssueToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FBA7B626730B9A0006E5E6 /* IssueToolbar.swift */; }; B3FBA7B9267335C30006E5E6 /* CancelButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FBA7B8267335C30006E5E6 /* CancelButton.swift */; }; + D0103BDE267B897E0079FC3D /* AddMilestoneViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0103BDD267B897E0079FC3D /* AddMilestoneViewController.swift */; }; + D0103BE0268066A80079FC3D /* AddMilestoneTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0103BDF268066A80079FC3D /* AddMilestoneTableViewCell.swift */; }; D03AF8F226771FA3001C2CBF /* Login.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D03AF8F126771FA3001C2CBF /* Login.storyboard */; }; D03AF8F42677253C001C2CBF /* IssueFilterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AF8F32677253C001C2CBF /* IssueFilterViewController.swift */; }; D03AF8F626774707001C2CBF /* UserList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AF8F526774707001C2CBF /* UserList.swift */; }; D03AF8F826774909001C2CBF /* MilestoneList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AF8F726774909001C2CBF /* MilestoneList.swift */; }; D03AF8FA2677494F001C2CBF /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AF8F92677494F001C2CBF /* Comment.swift */; }; + D04C694E267A35960056DD79 /* MilestoneViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04C694D267A35960056DD79 /* MilestoneViewModel.swift */; }; D04C6956267B2C560056DD79 /* ToolBarTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04C6955267B2C560056DD79 /* ToolBarTextField.swift */; }; D0A88E0226732D21005877F6 /* NewIssueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A88E0126732D21005877F6 /* NewIssueViewController.swift */; }; D0A88E04267334A6005877F6 /* IssueDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A88E03267334A6005877F6 /* IssueDetailViewController.swift */; }; - D0A88E072674D9F0005877F6 /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = D0A88E062674D9F0005877F6 /* MarkdownKit */; }; D0A88E092674F888005877F6 /* IssueDetailTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A88E082674F888005877F6 /* IssueDetailTableViewCell.swift */; }; D0A88E0D26762D51005877F6 /* ModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A88E0C26762D51005877F6 /* ModalViewController.swift */; }; D0A88E0F26765325005877F6 /* UIKit+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A88E0E26765325005877F6 /* UIKit+Extension.swift */; }; @@ -58,10 +72,12 @@ D0ADB6D5266F4F3D00E0762C /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D0ADB6D4266F4F3D00E0762C /* RxSwift */; }; D0ADB6D7266F4F4700E0762C /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = D0ADB6D6266F4F4700E0762C /* SnapKit */; }; D0ADB6F8266F5BBE00E0762C /* OAuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ADB6F7266F5BBE00E0762C /* OAuthManager.swift */; }; + D0DAAE28268071F00075E794 /* NewIssueViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DAAE27268071F00075E794 /* NewIssueViewModel.swift */; }; + D0DAAE2A2680A8DE0075E794 /* AdditionalInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DAAE292680A8DE0075E794 /* AdditionalInfoViewController.swift */; }; + D0DAAE2C2680E2E20075E794 /* IssueDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DAAE2B2680E2E20075E794 /* IssueDetailViewModel.swift */; }; D0FD509E26708DDE008C6031 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B559E4266E095E00901C55 /* LoginViewController.swift */; }; D0FD50AA267096C0008C6031 /* MilestoneViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FD50A9267096C0008C6031 /* MilestoneViewController.swift */; }; D0FD50BB26709F8B008C6031 /* MilestoneTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FD50BA26709F8B008C6031 /* MilestoneTableViewCell.swift */; }; - D0FD50D4267192AF008C6031 /* AddViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FD50D3267192AF008C6031 /* AddViewController.swift */; }; D0FD50F72671B156008C6031 /* LabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FD50F62671B156008C6031 /* LabelTableViewCell.swift */; }; /* End PBXBuildFile section */ @@ -84,6 +100,7 @@ /* Begin PBXFileReference section */ 3D239EFE5DEB1D1C49840391 /* Pods-issue-trackerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-issue-trackerTests.release.xcconfig"; path = "Target Support Files/Pods-issue-trackerTests/Pods-issue-trackerTests.release.xcconfig"; sourceTree = ""; }; + 491F08932679E5410081C5C5 /* AddLabelViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddLabelViewController.swift; sourceTree = ""; }; 49903DAA266F558300D2A6DD /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; 49903DAD266F588B00D2A6DD /* IDPasswordTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IDPasswordTextField.swift; sourceTree = ""; }; 57FD79E124256E10A8CD0326 /* Pods_issue_trackerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_issue_trackerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -100,6 +117,13 @@ B32FEACD267260E400BF37A1 /* AddIssueButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddIssueButton.swift; sourceTree = ""; }; B349997C266F8D0B0091A44A /* GitHubLoginButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubLoginButton.swift; sourceTree = ""; }; B349997E266F90710091A44A /* AppleLoginButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleLoginButton.swift; sourceTree = ""; }; + B34FF73D267AE9D8002D9C56 /* EstimatedLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EstimatedLabelView.swift; sourceTree = ""; }; + B34FF740267B8800002D9C56 /* AddLabelTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddLabelTableViewCell.swift; sourceTree = ""; }; + B366CA40268319AC00C97190 /* LeftAlignedCollectionViewFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeftAlignedCollectionViewFlowLayout.swift; sourceTree = ""; }; + B366CA42268370BA00C97190 /* CustomAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertView.swift; sourceTree = ""; }; + B3926D6A2681B59800F72CEE /* PatchIssue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchIssue.swift; sourceTree = ""; }; + B3A5CB57267840290060DC85 /* LabelListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelListViewModel.swift; sourceTree = ""; }; + B3A5CB5A26784AA50060DC85 /* LabelList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelList.swift; sourceTree = ""; }; B3B559DD266E095E00901C55 /* issue-tracker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "issue-tracker.app"; sourceTree = BUILT_PRODUCTS_DIR; }; B3B559E0266E095E00901C55 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; B3B559E2266E095E00901C55 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -114,8 +138,11 @@ B3B559FE266E096000901C55 /* issue-trackerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "issue-trackerUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; B3B55A02266E096000901C55 /* issue_trackerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = issue_trackerUITests.swift; sourceTree = ""; }; B3B55A04266E096000901C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B3BA32992680686E0009FE72 /* IssueListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueListViewModel.swift; sourceTree = ""; }; + B3CBD19A267C359500F8D733 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; B3D7D0C3267735CC000F02F4 /* IssueList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueList.swift; sourceTree = ""; }; B3D7D0C826773AEA000F02F4 /* NewIssue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewIssue.swift; sourceTree = ""; }; + B3E01192267A0B44001155D4 /* AddLabelViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddLabelViewModel.swift; sourceTree = ""; }; B3EADABD2675EFED0007C4B6 /* AddLabelButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddLabelButton.swift; sourceTree = ""; }; B3EADABF267628FB0007C4B6 /* LabelsCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelsCollectionView.swift; sourceTree = ""; }; B3EADAC1267629190007C4B6 /* LabelsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelsCollectionViewCell.swift; sourceTree = SOURCE_ROOT; }; @@ -125,11 +152,14 @@ B3FBA7B626730B9A0006E5E6 /* IssueToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = IssueToolbar.swift; path = "issue-tracker/Controller/IssueToolbar.swift"; sourceTree = SOURCE_ROOT; }; B3FBA7B8267335C30006E5E6 /* CancelButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelButton.swift; sourceTree = ""; }; CCF0F131FB8BE0BC0EF7FE4E /* Pods-issue-tracker.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-issue-tracker.release.xcconfig"; path = "Target Support Files/Pods-issue-tracker/Pods-issue-tracker.release.xcconfig"; sourceTree = ""; }; + D0103BDD267B897E0079FC3D /* AddMilestoneViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddMilestoneViewController.swift; sourceTree = ""; }; + D0103BDF268066A80079FC3D /* AddMilestoneTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddMilestoneTableViewCell.swift; sourceTree = ""; }; D03AF8F126771FA3001C2CBF /* Login.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Login.storyboard; sourceTree = ""; }; D03AF8F32677253C001C2CBF /* IssueFilterViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueFilterViewController.swift; sourceTree = ""; }; D03AF8F526774707001C2CBF /* UserList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserList.swift; sourceTree = ""; }; D03AF8F726774909001C2CBF /* MilestoneList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MilestoneList.swift; sourceTree = ""; }; D03AF8F92677494F001C2CBF /* Comment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; + D04C694D267A35960056DD79 /* MilestoneViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MilestoneViewModel.swift; sourceTree = ""; }; D04C6955267B2C560056DD79 /* ToolBarTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToolBarTextField.swift; sourceTree = ""; }; D0A88E0126732D21005877F6 /* NewIssueViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewIssueViewController.swift; sourceTree = ""; }; D0A88E03267334A6005877F6 /* IssueDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueDetailViewController.swift; sourceTree = ""; }; @@ -140,9 +170,12 @@ D0ADB693266F0A5000E0762C /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = ""; }; D0ADB698266F0C1300E0762C /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; D0ADB6F7266F5BBE00E0762C /* OAuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthManager.swift; sourceTree = ""; }; + D0DAAE27268071F00075E794 /* NewIssueViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewIssueViewModel.swift; sourceTree = ""; }; + D0DAAE292680A8DE0075E794 /* AdditionalInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdditionalInfoViewController.swift; sourceTree = ""; }; + D0DAAE2B2680E2E20075E794 /* IssueDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueDetailViewModel.swift; sourceTree = ""; }; D0FD50A9267096C0008C6031 /* MilestoneViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MilestoneViewController.swift; sourceTree = ""; }; D0FD50BA26709F8B008C6031 /* MilestoneTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MilestoneTableViewCell.swift; sourceTree = ""; }; - D0FD50D3267192AF008C6031 /* AddViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddViewController.swift; sourceTree = ""; }; + D0FD50DB2671AA04008C6031 /* InputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputView.swift; sourceTree = ""; }; D0FD50F62671B156008C6031 /* LabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelTableViewCell.swift; sourceTree = ""; }; E7F242466FC866CAD16EFF69 /* Pods-issue-tracker.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-issue-tracker.debug.xcconfig"; path = "Target Support Files/Pods-issue-tracker/Pods-issue-tracker.debug.xcconfig"; sourceTree = ""; }; F0166F39C919837292E32AEF /* Pods_issue_tracker_issue_trackerUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_issue_tracker_issue_trackerUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -157,7 +190,7 @@ 9F61736E94FA27116F3653A8 /* Pods_issue_tracker.framework in Frameworks */, D0ADB6D5266F4F3D00E0762C /* RxSwift in Frameworks */, D0ADB6CC266F4F1C00E0762C /* Alamofire in Frameworks */, - D0A88E072674D9F0005877F6 /* MarkdownKit in Frameworks */, + B3E01196267A28B2001155D4 /* MarkdownKit in Frameworks */, D0ADB6D7266F4F4700E0762C /* SnapKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -184,11 +217,11 @@ 49903DAC266F558700D2A6DD /* View */ = { isa = PBXGroup; children = ( + B366CA44268370C100C97190 /* Alert */, B32FEAC42671DCD200BF37A1 /* IssueList */, D0FD50F12671B108008C6031 /* Label */, D0FD50ED2671B0E8008C6031 /* Milestone */, B3F5274F26709614002B0812 /* Login */, - D0A88E082674F888005877F6 /* IssueDetailTableViewCell.swift */, ); path = View; sourceTree = ""; @@ -196,6 +229,7 @@ B32FEAC42671DCD200BF37A1 /* IssueList */ = { isa = PBXGroup; children = ( + B366CA40268319AC00C97190 /* LeftAlignedCollectionViewFlowLayout.swift */, D04C6955267B2C560056DD79 /* ToolBarTextField.swift */, B3EADAC1267629190007C4B6 /* LabelsCollectionViewCell.swift */, B3EADABF267628FB0007C4B6 /* LabelsCollectionView.swift */, @@ -207,19 +241,51 @@ B32FEAC72671F65F00BF37A1 /* FilterBarButton.swift */, B3FBA7B8267335C30006E5E6 /* CancelButton.swift */, B32FEAC92671FD1600BF37A1 /* SelectBarButton.swift */, + D0A88E082674F888005877F6 /* IssueDetailTableViewCell.swift */, ); path = IssueList; sourceTree = ""; }; + B34FF73F267B83C2002D9C56 /* Recovered References */ = { + isa = PBXGroup; + children = ( + D0FD50DB2671AA04008C6031 /* InputView.swift */, + ); + name = "Recovered References"; + sourceTree = ""; + }; + B366CA44268370C100C97190 /* Alert */ = { + isa = PBXGroup; + children = ( + B366CA42268370BA00C97190 /* CustomAlertView.swift */, + ); + path = Alert; + sourceTree = ""; + }; + B3A5CB59267840310060DC85 /* ViewModel */ = { + isa = PBXGroup; + children = ( + B3A5CB57267840290060DC85 /* LabelListViewModel.swift */, + B3BA32992680686E0009FE72 /* IssueListViewModel.swift */, + B3E01192267A0B44001155D4 /* AddLabelViewModel.swift */, + D04C694D267A35960056DD79 /* MilestoneViewModel.swift */, + D0DAAE27268071F00075E794 /* NewIssueViewModel.swift */, + D0DAAE2B2680E2E20075E794 /* IssueDetailViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; B3B559D4266E095E00901C55 = { isa = PBXGroup; children = ( + B3CBD19A267C359500F8D733 /* .swiftlint.yml */, B3B559DF266E095E00901C55 /* issue-tracker */, B3B559F6266E096000901C55 /* issue-trackerTests */, B3B55A01266E096000901C55 /* issue-trackerUITests */, B3B559DE266E095E00901C55 /* Products */, DD720597B58AE4F3E0B9F4F7 /* Pods */, E53A20619409B79F9C593158 /* Frameworks */, + B34FF73F267B83C2002D9C56 /* Recovered References */, ); sourceTree = ""; }; @@ -239,6 +305,7 @@ B3B559EE266E096000901C55 /* Info.plist */, B3B559E6266E095E00901C55 /* Main.storyboard */, D0ADB68F266F0A2200E0762C /* Supporting Files */, + B3A5CB59267840310060DC85 /* ViewModel */, D0FD50E92671B0AD008C6031 /* Controller */, B3F527562670A0D5002B0812 /* Extension */, 49903DAC266F558700D2A6DD /* View */, @@ -271,6 +338,8 @@ isa = PBXGroup; children = ( B3D7D0C3267735CC000F02F4 /* IssueList.swift */, + B3926D6A2681B59800F72CEE /* PatchIssue.swift */, + B3A5CB5A26784AA50060DC85 /* LabelList.swift */, D03AF8F92677494F001C2CBF /* Comment.swift */, D03AF8F726774909001C2CBF /* MilestoneList.swift */, B3D7D0C826773AEA000F02F4 /* NewIssue.swift */, @@ -326,13 +395,15 @@ children = ( D0A88E0C26762D51005877F6 /* ModalViewController.swift */, B32FEAC02671D9F600BF37A1 /* IssueListViewController.swift */, + D03AF8F32677253C001C2CBF /* IssueFilterViewController.swift */, B3B559E4266E095E00901C55 /* LoginViewController.swift */, B3F527502670968F002B0812 /* LabelViewController.swift */, D0FD50A9267096C0008C6031 /* MilestoneViewController.swift */, - D0FD50D3267192AF008C6031 /* AddViewController.swift */, + 491F08932679E5410081C5C5 /* AddLabelViewController.swift */, D0A88E0126732D21005877F6 /* NewIssueViewController.swift */, D0A88E03267334A6005877F6 /* IssueDetailViewController.swift */, - D03AF8F32677253C001C2CBF /* IssueFilterViewController.swift */, + D0103BDD267B897E0079FC3D /* AddMilestoneViewController.swift */, + D0DAAE292680A8DE0075E794 /* AdditionalInfoViewController.swift */, ); path = Controller; sourceTree = ""; @@ -340,6 +411,7 @@ D0FD50ED2671B0E8008C6031 /* Milestone */ = { isa = PBXGroup; children = ( + D0103BDF268066A80079FC3D /* AddMilestoneTableViewCell.swift */, D0FD50BA26709F8B008C6031 /* MilestoneTableViewCell.swift */, ); path = Milestone; @@ -351,6 +423,8 @@ D0FD50F62671B156008C6031 /* LabelTableViewCell.swift */, B3EADABD2675EFED0007C4B6 /* AddLabelButton.swift */, B3F5275B2670C0EF002B0812 /* PaddingLabel.swift */, + B34FF73D267AE9D8002D9C56 /* EstimatedLabelView.swift */, + B34FF740267B8800002D9C56 /* AddLabelTableViewCell.swift */, ); path = Label; sourceTree = ""; @@ -389,6 +463,7 @@ B3B559D9266E095E00901C55 /* Sources */, B3B559DA266E095E00901C55 /* Frameworks */, B3B559DB266E095E00901C55 /* Resources */, + B3CBD199267C34F200F8D733 /* ShellScript */, ); buildRules = ( ); @@ -400,7 +475,7 @@ D0ADB6D0266F4F3D00E0762C /* RxCocoa */, D0ADB6D4266F4F3D00E0762C /* RxSwift */, D0ADB6D6266F4F4700E0762C /* SnapKit */, - D0A88E062674D9F0005877F6 /* MarkdownKit */, + B3E01195267A28B2001155D4 /* MarkdownKit */, ); productName = "issue-tracker"; productReference = B3B559DD266E095E00901C55 /* issue-tracker.app */; @@ -479,7 +554,7 @@ B3B55A10266E0C8600901C55 /* XCRemoteSwiftPackageReference "Alamofire" */, B3B55A13266E0D5600901C55 /* XCRemoteSwiftPackageReference "RxSwift" */, B3F50EE7266E12340009C260 /* XCRemoteSwiftPackageReference "SnapKit" */, - D0A88E052674D9F0005877F6 /* XCRemoteSwiftPackageReference "MarkdownKit" */, + B3E01194267A28B1001155D4 /* XCRemoteSwiftPackageReference "MarkdownKit" */, ); productRefGroup = B3B559DE266E095E00901C55 /* Products */; projectDirPath = ""; @@ -497,6 +572,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + B3CBD19B267C359500F8D733 /* .swiftlint.yml in Resources */, D0A88E112676563E005877F6 /* Modal.storyboard in Resources */, B3B559ED266E096000901C55 /* LaunchScreen.storyboard in Resources */, B3B559EA266E096000901C55 /* Assets.xcassets in Resources */, @@ -566,6 +642,23 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + B3CBD199267C34F200F8D733 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "${PODS_ROOT}/SwiftLint/swiftlint\n"; + }; F949EFDA4F0A02CAD51691D6 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -595,44 +688,59 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B366CA43268370BA00C97190 /* CustomAlertView.swift in Sources */, B3F527512670968F002B0812 /* LabelViewController.swift in Sources */, D0A88E0226732D21005877F6 /* NewIssueViewController.swift in Sources */, B3FBA7B726730B9A0006E5E6 /* IssueToolbar.swift in Sources */, D0FD509E26708DDE008C6031 /* LoginViewController.swift in Sources */, B32FEACC26724CF400BF37A1 /* IssueTableFooterView.swift in Sources */, + B3A5CB5B26784AA50060DC85 /* LabelList.swift in Sources */, B3EADABE2675EFED0007C4B6 /* AddLabelButton.swift in Sources */, + 491F08942679E5420081C5C5 /* AddLabelViewController.swift in Sources */, + B3BA329A2680686E0009FE72 /* IssueListViewModel.swift in Sources */, B32FEAC12671D9F600BF37A1 /* IssueListViewController.swift in Sources */, B32FEACA2671FD1600BF37A1 /* SelectBarButton.swift in Sources */, D0A88E0F26765325005877F6 /* UIKit+Extension.swift in Sources */, + B366CA41268319AC00C97190 /* LeftAlignedCollectionViewFlowLayout.swift in Sources */, B3F5275C2670C0EF002B0812 /* PaddingLabel.swift in Sources */, + B3A5CB58267840290060DC85 /* LabelListViewModel.swift in Sources */, B32FEAC32671DCB100BF37A1 /* IssueTableViewCell.swift in Sources */, 49903DAB266F558300D2A6DD /* LoginView.swift in Sources */, D0FD50F72671B156008C6031 /* LabelTableViewCell.swift in Sources */, - D0FD50D4267192AF008C6031 /* AddViewController.swift in Sources */, D04C6956267B2C560056DD79 /* ToolBarTextField.swift in Sources */, + D0DAAE28268071F00075E794 /* NewIssueViewModel.swift in Sources */, B32FEACE267260E400BF37A1 /* AddIssueButton.swift in Sources */, + B34FF741267B8800002D9C56 /* AddLabelTableViewCell.swift in Sources */, 49903DAE266F588B00D2A6DD /* IDPasswordTextField.swift in Sources */, B349997D266F8D0B0091A44A /* GitHubLoginButton.swift in Sources */, + D0103BE0268066A80079FC3D /* AddMilestoneTableViewCell.swift in Sources */, B3F527552670A0CD002B0812 /* UIColor+HexInit.swift in Sources */, D03AF8F626774707001C2CBF /* UserList.swift in Sources */, B349997F266F90710091A44A /* AppleLoginButton.swift in Sources */, D0A88E04267334A6005877F6 /* IssueDetailViewController.swift in Sources */, B32FEAC82671F65F00BF37A1 /* FilterBarButton.swift in Sources */, + D04C694E267A35960056DD79 /* MilestoneViewModel.swift in Sources */, D0ADB6F8266F5BBE00E0762C /* OAuthManager.swift in Sources */, B3B559E1266E095E00901C55 /* AppDelegate.swift in Sources */, D0A88E0D26762D51005877F6 /* ModalViewController.swift in Sources */, + D0103BDE267B897E0079FC3D /* AddMilestoneViewController.swift in Sources */, D0A88E092674F888005877F6 /* IssueDetailTableViewCell.swift in Sources */, B3D7D0C4267735CC000F02F4 /* IssueList.swift in Sources */, + B3926D6B2681B59800F72CEE /* PatchIssue.swift in Sources */, D0ADB699266F0C1300E0762C /* NetworkManager.swift in Sources */, B3FBA7B9267335C30006E5E6 /* CancelButton.swift in Sources */, D03AF8FA2677494F001C2CBF /* Comment.swift in Sources */, + B34FF73E267AE9D8002D9C56 /* EstimatedLabelView.swift in Sources */, D03AF8F42677253C001C2CBF /* IssueFilterViewController.swift in Sources */, D0ADB694266F0A5000E0762C /* Endpoint.swift in Sources */, B3D7D0C926773AEA000F02F4 /* NewIssue.swift in Sources */, B3B559E3266E095E00901C55 /* SceneDelegate.swift in Sources */, + B3E01193267A0B44001155D4 /* AddLabelViewModel.swift in Sources */, B32FEAC62671DDA600BF37A1 /* MilestoneView.swift in Sources */, + D0DAAE2C2680E2E20075E794 /* IssueDetailViewModel.swift in Sources */, B3EADAC0267628FB0007C4B6 /* LabelsCollectionView.swift in Sources */, B3EADAC2267629190007C4B6 /* LabelsCollectionViewCell.swift in Sources */, + D0DAAE2A2680A8DE0075E794 /* AdditionalInfoViewController.swift in Sources */, D0FD50AA267096C0008C6031 /* MilestoneViewController.swift in Sources */, D0FD50BB26709F8B008C6031 /* MilestoneTableViewCell.swift in Sources */, D03AF8F826774909001C2CBF /* MilestoneList.swift in Sources */, @@ -992,28 +1100,28 @@ minimumVersion = 6.2.0; }; }; - B3F50EE7266E12340009C260 /* XCRemoteSwiftPackageReference "SnapKit" */ = { + B3E01194267A28B1001155D4 /* XCRemoteSwiftPackageReference "MarkdownKit" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/SnapKit/SnapKit.git"; + repositoryURL = "https://github.com/bmoliveira/MarkdownKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 5.0.1; + minimumVersion = 1.7.0; }; }; - D0A88E052674D9F0005877F6 /* XCRemoteSwiftPackageReference "MarkdownKit" */ = { + B3F50EE7266E12340009C260 /* XCRemoteSwiftPackageReference "SnapKit" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/bmoliveira/MarkdownKit.git"; + repositoryURL = "https://github.com/SnapKit/SnapKit.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.7.1; + minimumVersion = 5.0.1; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - D0A88E062674D9F0005877F6 /* MarkdownKit */ = { + B3E01195267A28B2001155D4 /* MarkdownKit */ = { isa = XCSwiftPackageProductDependency; - package = D0A88E052674D9F0005877F6 /* XCRemoteSwiftPackageReference "MarkdownKit" */; + package = B3E01194267A28B1001155D4 /* XCRemoteSwiftPackageReference "MarkdownKit" */; productName = MarkdownKit; }; D0ADB6CB266F4F1C00E0762C /* Alamofire */ = { diff --git a/iOS/issue-tracker/issue-tracker.xcworkspace/xcshareddata/swiftpm/Package.resolved b/iOS/issue-tracker/issue-tracker.xcworkspace/xcshareddata/swiftpm/Package.resolved index 88324d757..779a725f9 100644 --- a/iOS/issue-tracker/issue-tracker.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/iOS/issue-tracker/issue-tracker.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -12,7 +12,7 @@ }, { "package": "MarkdownKit", - "repositoryURL": "https://github.com/bmoliveira/MarkdownKit.git", + "repositoryURL": "https://github.com/bmoliveira/MarkdownKit", "state": { "branch": null, "revision": "5056f3305d3499f44d8815530d560b87082e0cf5", diff --git a/iOS/issue-tracker/issue-tracker/Base.lproj/Main.storyboard b/iOS/issue-tracker/issue-tracker/Base.lproj/Main.storyboard index 72b54dcc0..71140a572 100644 --- a/iOS/issue-tracker/issue-tracker/Base.lproj/Main.storyboard +++ b/iOS/issue-tracker/issue-tracker/Base.lproj/Main.storyboard @@ -17,7 +17,7 @@ - + @@ -71,7 +71,7 @@ - + @@ -81,7 +81,7 @@ - + @@ -106,7 +106,7 @@ - + diff --git a/iOS/issue-tracker/issue-tracker/Controller/AddLabelViewController.swift b/iOS/issue-tracker/issue-tracker/Controller/AddLabelViewController.swift new file mode 100644 index 000000000..ed48ee62c --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Controller/AddLabelViewController.swift @@ -0,0 +1,138 @@ +// +// AddViewController.swift +// issue-tracker +// +// Created by Ador on 2021/06/10. +// + +import UIKit +import RxSwift +import RxCocoa +import SnapKit + +protocol AddLabelViewControllerDelegate: AnyObject { + func fetchData() +} + +final class AddLabelViewController: UIViewController { + private var addLabelViewModel: AddLabelViewModel! + private lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .grouped) + tableView.allowsSelection = false + tableView.register(AddLabelTableViewCell.self, forCellReuseIdentifier: AddLabelTableViewCell.identifier) + return tableView + }() + private var estimatedLabelView = EstimatedLabelView() + private var bag = DisposeBag() + private var saveButton = UIBarButtonItem(title: "저장", style: .plain, target: nil, action: nil) + private var cancelButton = UIBarButtonItem(title: "뒤로", style: .plain, target: nil, action: nil) + var reloadDataHandler: (() -> Void)? + weak var delegate: AddLabelViewControllerDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + self.view.backgroundColor = #colorLiteral(red: 0.9494308829, green: 0.9485411048, blue: 0.9703034759, alpha: 1) + self.addLabelViewModel = AddLabelViewModel(networkManager: NetworkManager()) + + navigationItem.title = "새로운 레이블" + self.navigationItem.leftBarButtonItem = cancelButton + self.navigationItem.rightBarButtonItem = saveButton + + tableView.dataSource = self + view.addSubview(tableView) + view.addSubview(estimatedLabelView) + configureAutolayout() + binding() + bindButton() + } + + private func binding() { + addLabelViewModel.color + .map { UIColor.hexStringToUIColor(hex: $0)} + .bind(to: estimatedLabelView.getLabel().rx.backgroundColor) + .disposed(by: bag) + } + + func bindButton() { + saveButton.rx.tap + .subscribe { [weak self] _ in + guard let viewModel = self?.addLabelViewModel else { return } + if viewModel.title.value == "" || viewModel.description.value == "" { + CustomAlertView.shared.setUpAlertView(title: "실패", message: "필수 입력란이 비었습니다. 다시 한번 확인해주세요.", buttonTitle: "확인", alertType: .failure, buttonHandler: nil) + } else { + viewModel.postAddedLabel { + self?.dismiss(animated: true, completion: nil) + self?.delegate?.fetchData() + } + } + } + .disposed(by: bag) + + cancelButton.rx.tap + .subscribe { [weak self] _ in + self?.dismiss(animated: true, completion: nil) + } + .disposed(by: bag) + } + + private func configureAutolayout() { + tableView.snp.makeConstraints { view in + view.top.equalToSuperview().inset(40) + view.leading.trailing.bottom.equalToSuperview() + } + + estimatedLabelView.snp.makeConstraints { view in + view.leading.trailing.equalToSuperview().inset(16) + view.centerX.centerY.equalToSuperview() + view.height.equalTo(160) + } + } +} + +extension AddLabelViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 3 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: AddLabelTableViewCell.identifier, for: indexPath) as? AddLabelTableViewCell else { return UITableViewCell() } + switch indexPath.row { + case 0: + cell.textLabel?.text = "제목" + cell.configureTextFieldPlaceHolder(text: "(필수입력)") + cell.textField.rx.text + .orEmpty + .distinctUntilChanged() + .observe(on: MainScheduler.instance) + .bind(to: addLabelViewModel.title) + .disposed(by: bag) + return cell + case 1: + cell.textLabel?.text = "설명" + cell.configureTextFieldPlaceHolder(text: "(선택사항)") + cell.textField.rx.text + .orEmpty + .distinctUntilChanged() + .observe(on: MainScheduler.instance) + .bind(to: addLabelViewModel.description) + .disposed(by: bag) + return cell + case 2: + cell.textLabel?.text = "배경색" + + addLabelViewModel.color + .bind(to: cell.textField.rx.text) + .disposed(by: bag) + + cell.reloadButton.rx.tap + .map { UIColor.getRandomColor().convertHexToString() } + .bind(to: addLabelViewModel.color) + .disposed(by: bag) + + cell.configureBackgroundCellMode() + return cell + default: + fatalError() + } + } +} diff --git a/iOS/issue-tracker/issue-tracker/Controller/AddMilestoneViewController.swift b/iOS/issue-tracker/issue-tracker/Controller/AddMilestoneViewController.swift new file mode 100644 index 000000000..f2d326925 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Controller/AddMilestoneViewController.swift @@ -0,0 +1,83 @@ +// +// AddMilestoneViewController.swift +// issue-tracker +// +// Created by Ador on 2021/06/17. +// + +import UIKit +import RxSwift + +class AddMilestoneViewController: UIViewController { + + private let disposeBag = DisposeBag() + private let viewModel: MilestoneViewModel! = MilestoneViewModel.shared + private lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .grouped) + tableView.register(AddMilestoneTableViewCell.self, forCellReuseIdentifier: AddMilestoneTableViewCell.reuseIdentifier) + return tableView + }() + + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.title = "새로운 마일스톤" + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "뒤로", + style: .plain, + target: self, + action: #selector(didTapCancel)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "저장", + style: .plain, + target: self, + action: #selector(didTapSave)) + + tableView.dataSource = self + tableView.frame = view.bounds + + view.addSubview(tableView) + viewModel.dismissCompletion = { + self.dismiss(animated: true) + } + } + + @objc + private func didTapSave() { + viewModel.post() + } + + @objc + private func didTapCancel() { + dismiss(animated: true) + } +} + +extension AddMilestoneViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 3 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: AddMilestoneTableViewCell.reuseIdentifier, for: indexPath) as? AddMilestoneTableViewCell else { + fatalError() + } + if indexPath.row == 0 { + cell.becomeFirstResponder() + } + let textLabel = ["제목", "설명", "완료일"] + let keys = ["title", "description", "dueDate"] + cell.textLabel?.text = textLabel[indexPath.row] + cell.bind { textField in + textField.rx.text + .orEmpty + .observe(on: MainScheduler.instance) + .distinctUntilChanged() + .debounce(.milliseconds(500), scheduler: MainScheduler.instance) + .subscribe(onNext: { text in + let key = keys[indexPath.row] + self.viewModel.milestone[key] = text + }) + .disposed(by: disposeBag) + } + return cell + } +} diff --git a/iOS/issue-tracker/issue-tracker/Controller/AddViewController.swift b/iOS/issue-tracker/issue-tracker/Controller/AddViewController.swift deleted file mode 100644 index 3b6512812..000000000 --- a/iOS/issue-tracker/issue-tracker/Controller/AddViewController.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// AddViewController.swift -// issue-tracker -// -// Created by Ador on 2021/06/10. -// - -import UIKit - -class AddViewController: UIViewController { - private let cellReuseIdentifier = "AddViewControllerCell" - private lazy var tableView: UITableView = { - let tableView = UITableView(frame: .zero, style: .grouped) - tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellReuseIdentifier) - return tableView - }() - - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.title = "새로운 마일스톤" - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "뒤로", - style: .plain, - target: self, - action: nil) - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "저장", - style: .plain, - target: self, - action: nil) - - tableView.dataSource = self - tableView.frame = view.bounds - - view.addSubview(tableView) - } -} - -extension AddViewController: UITableViewDataSource { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 3 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath) - let textField = UITextField(frame: CGRect(x: cell.frame.origin.x + 120, y: cell.frame.origin.y, width: cell.frame.width - 140, height: 44)) - cell.contentView.addSubview(textField) - switch indexPath.row { - case 0: - cell.textLabel?.text = "제목" - return cell - case 1: - cell.textLabel?.text = "설명" - return cell - case 2: - cell.textLabel?.text = "완료일" - return cell - default: - assert(indexPath.row > 2, "index path out of range") - } - return cell - } -} diff --git a/iOS/issue-tracker/issue-tracker/Controller/AdditionalInfoViewController.swift b/iOS/issue-tracker/issue-tracker/Controller/AdditionalInfoViewController.swift new file mode 100644 index 000000000..c4a79b47d --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Controller/AdditionalInfoViewController.swift @@ -0,0 +1,68 @@ +// +// AdditionalInfoViewController.swift +// issue-tracker +// +// Created by Ador on 2021/06/21. +// + +import UIKit + +class AdditionalInfoViewController: UITableViewController { + // temp + let data = ["enhancement", "bug", "feature"] + var setupSelectedData: ((String) -> Void)? + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + tableView.register(DetailTextStyleTableViewCell.self, forCellReuseIdentifier: "reuseIdentifier") + + self.navigationItem.leftBarButtonItem + = UIBarButtonItem(title: "취소", style: .done, target: self, action: #selector(didTapCancel)) + self.navigationItem.rightBarButtonItem + = UIBarButtonItem(title: "저장", style: .done, target: self, action: #selector(didTapSave)) + } + + // MARK: - Table view data source + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return data.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath) + cell.textLabel?.text = data[indexPath.row] + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let cell = tableView.cellForRow(at: indexPath) + cell?.accessoryType = .checkmark + } + + override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { + let selected = tableView.cellForRow(at: indexPath) + selected?.accessoryType = .none + } + + override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + return UIView() + } +} + +private extension AdditionalInfoViewController { + @objc + func didTapCancel() { + dismiss(animated: true) + } + + @objc + func didTapSave() { + guard let indexPath = tableView.indexPathForSelectedRow else { + didTapCancel() + return + } + setupSelectedData?(data[indexPath.row]) + didTapCancel() + } +} diff --git a/iOS/issue-tracker/issue-tracker/Controller/IssueDetailViewController.swift b/iOS/issue-tracker/issue-tracker/Controller/IssueDetailViewController.swift index b31d99d80..ab4fb4a03 100644 --- a/iOS/issue-tracker/issue-tracker/Controller/IssueDetailViewController.swift +++ b/iOS/issue-tracker/issue-tracker/Controller/IssueDetailViewController.swift @@ -7,12 +7,43 @@ import UIKit import SnapKit +import RxSwift +import RxCocoa -class IssueDetailViewController: UIViewController { +final class IssueDetailViewController: UIViewController { + + var viewModel: IssueDetailViewModel = IssueDetailViewModel() private let cellReuseIdentifier = "IssueDetailCell" - private let data = [1, 2, 3, 4, 5, 6, 7, 8, 9] - + private let disposeBag = DisposeBag() + private var comment: [Comment] = [] + private var textFieldHeightConstraint: NSLayoutConstraint? + + private let headerStackView: UIStackView = { + let stackView = UIStackView(frame: CGRect(origin: .zero, size: CGSize(width: 1, height: 44))) + stackView.axis = .vertical + stackView.alignment = .leading + stackView.spacing = 10 + return stackView + }() + + private let isOpened: PaddingLabel = { + let label = PaddingLabel(withInsets: 0, 0, 10, 10) + label.textAlignment = .center + label.backgroundColor = .systemPink + label.textColor = .white + label.layer.masksToBounds = true + label.layer.cornerRadius = 10 + label.snp.makeConstraints { $0.width.greaterThanOrEqualTo(50) } + return label + }() + + private let authorLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + return label + }() + private let tableView: UITableView = { let tableView = UITableView() tableView.rowHeight = 130 @@ -20,67 +51,49 @@ class IssueDetailViewController: UIViewController { return tableView }() - private let toolbar: UIToolbar = { - let toolbar = UIToolbar(frame: CGRect(origin: .zero, size: CGSize(width: 100, height: 100))) - let textField = ToolBarTextField(frame: toolbar.bounds) - let up = UIBarButtonItem(image: UIImage(systemName: "chevron.up.circle"), - style: .plain, target: self, action: #selector(scrollToBefore)) - let down = UIBarButtonItem(image: UIImage(systemName: "chevron.down.circle"), - style: .plain, target: self, action: #selector(scrollToNext)) - let comment = UIBarButtonItem(customView: textField) - let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: comment, action: nil) - toolbar.setItems([up, down, space, comment], animated: false) - return toolbar - }() - + private let textField = ToolBarTextField() + + deinit { + NotificationCenter.default.removeObserver(self) + } + override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .systemBackground - navigationController?.navigationBar.prefersLargeTitles = true + + navigationItem.largeTitleDisplayMode = .always navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis"), style: .plain, target: self, action: #selector(showIssueDetailInfo)) - navigationItem.title = "테스트 이슈 #2" - + + textField.textFieldDelegate = self tableView.dataSource = self tableView.delegate = self - tableView.frame = view.bounds tableView.selectRow(at: IndexPath(row: 0, section: 0), animated: false, scrollPosition: .none) - + + headerStackView.addArrangedSubview(isOpened) + headerStackView.addArrangedSubview(authorLabel) + + tableView.tableHeaderView = headerStackView + view.addSubview(tableView) - view.addSubview(toolbar) - + view.addSubview(textField) + setupAutolayout() + setupKeyboardNotification() + bind() + + view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTapGesture))) } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true } - private func setupAutolayout() { - toolbar.snp.makeConstraints { maker in - maker.leading.trailing.equalToSuperview() - maker.bottom.equalTo(view.safeAreaLayoutGuide) - maker.height.equalTo(44) - } - } - - @objc - private func scrollToBefore() { - guard let indexPath = tableView.indexPathForSelectedRow, - indexPath.row != 0 else { return } - tableView.selectRow(at: IndexPath(row: indexPath.row - 1, section: indexPath.section), - animated: true, scrollPosition: .top) - } - @objc - private func scrollToNext() { - guard let indexPath = tableView.indexPathForSelectedRow, - indexPath.row + 1 < data.count else { return } - tableView.selectRow(at: IndexPath(row: indexPath.row + 1, section: indexPath.section), - animated: true, scrollPosition: .bottom) + private func handleTapGesture(recognizer: UITapGestureRecognizer) { + textField.resignFirstResponder() } - + @objc private func showIssueDetailInfo() { let storyboard = UIStoryboard(name: "Modal", bundle: nil) @@ -88,16 +101,67 @@ class IssueDetailViewController: UIViewController { controller.modalPresentationStyle = .custom present(controller, animated: true, completion: nil) } + + func fetchData(id: Int) { + viewModel.fetch(id: id) + } +} + +private extension IssueDetailViewController { + func bind() { + viewModel.subject.bind { [weak self] detail in + guard let detail = detail?.data else { return } + self?.navigationItem.title = detail.title + self?.isOpened.text = detail.open ? "Open" : "Closed" + self?.authorLabel.text = "\(detail.author.name)님이 작성했습니다." + guard let comment = detail.comment else { return } + self?.comment = comment + self?.tableView.reloadData() + } + .disposed(by: disposeBag) + } + + func setupKeyboardNotification() { + let center = NotificationCenter.default + center.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { [weak self] noti in + guard let strongSelf = self else { return } + if let keyboardFrame = noti.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue { + strongSelf.textFieldHeightConstraint?.constant = -(keyboardFrame.cgRectValue.height - strongSelf.bottomSafeAreaHeight) + } + } + center.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { [weak self] _ in + guard let strongSelf = self else { return } + strongSelf.textFieldHeightConstraint?.constant = 0 + } + } + + func setupAutolayout() { + headerStackView.snp.makeConstraints { $0.leading.trailing.top.bottom.equalToSuperview().inset(20) } + tableView.snp.makeConstraints { + $0.leading.trailing.top.bottom.equalTo(view.safeAreaLayoutGuide) } + NSLayoutConstraint.activate([ + textField.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10), + textField.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10), + textField.heightAnchor.constraint(equalToConstant: 44) + ]) + textFieldHeightConstraint = textField.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) + textFieldHeightConstraint?.isActive = true + } } extension IssueDetailViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return data.count + return comment.count } - + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath) + guard let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath) as? IssueDetailTableViewCell else { + let cell = UITableViewCell() + cell.textLabel?.text = "아직 코멘트가 없습니다..." + return cell + } cell.accessoryView = UIImageView(image: UIImage(systemName: "ellipsis")) + cell.configure(model: comment[indexPath.row]) return cell } } @@ -107,3 +171,12 @@ extension IssueDetailViewController: UITableViewDelegate { UIView() } } + +extension IssueDetailViewController: ToolBarTextFieldDelegate { + func register() { + guard let comment = textField.text else { + return + } + viewModel.post(comment: comment) + } +} diff --git a/iOS/issue-tracker/issue-tracker/Controller/IssueFilterViewController.swift b/iOS/issue-tracker/issue-tracker/Controller/IssueFilterViewController.swift index 229fbb155..5ae2954a4 100644 --- a/iOS/issue-tracker/issue-tracker/Controller/IssueFilterViewController.swift +++ b/iOS/issue-tracker/issue-tracker/Controller/IssueFilterViewController.swift @@ -9,6 +9,7 @@ import UIKit class IssueFilterViewController: UIViewController { + var viewModel: IssueListViewModel! private let cellReuseIdentifier = "IssueFilterTableViewCell" private lazy var tableView: UITableView = { let tableView = UITableView(frame: .zero, style: .grouped) @@ -20,10 +21,10 @@ class IssueFilterViewController: UIViewController { var authors = ["Zeke", "Soo"] private let issueFilter = ["열린 이슈", "내가 작성한 이슈", "나에게 할당된 이슈", "내가 댓글을 남긴 이슈", "닫힌 이슈"] private let issueLabels = ["레이블 없음", "bug", "feature"] - + override func viewDidLoad() { super.viewDidLoad() - + navigationItem.title = "필터" self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "취소", style: .plain, @@ -32,18 +33,30 @@ class IssueFilterViewController: UIViewController { self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "저장", style: .plain, target: self, - action: nil) + action: #selector(saveButtonTapped)) tableView.delegate = self tableView.dataSource = self tableView.frame = view.bounds - + view.addSubview(tableView) } - + @objc private func cancelButtonTapped() { dismiss(animated: true) } + + @objc + private func saveButtonTapped() { + let indexPath = tableView.indexPathForSelectedRow + if indexPath == [0, 0] { + viewModel.fetchIssueList(filterBy: "open") + } + if indexPath == [0, 4] { + viewModel.fetchIssueList(filterBy: "closed") + } + dismiss(animated: true) + } } extension IssueFilterViewController: UITableViewDelegate { @@ -59,12 +72,12 @@ extension IssueFilterViewController: UITableViewDelegate { return nil } } - + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let selected = tableView.cellForRow(at: indexPath) selected?.accessoryType = .checkmark } - + func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { let selected = tableView.cellForRow(at: indexPath) selected?.accessoryType = .none @@ -75,7 +88,7 @@ extension IssueFilterViewController: UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { return 3 } - + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { switch section { case 0: @@ -88,7 +101,7 @@ extension IssueFilterViewController: UITableViewDataSource { return 0 } } - + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath) cell.selectionStyle = .none diff --git a/iOS/issue-tracker/issue-tracker/Controller/IssueTableFooterView.swift b/iOS/issue-tracker/issue-tracker/Controller/IssueTableFooterView.swift index 144c66843..59341005d 100644 --- a/iOS/issue-tracker/issue-tracker/Controller/IssueTableFooterView.swift +++ b/iOS/issue-tracker/issue-tracker/Controller/IssueTableFooterView.swift @@ -8,30 +8,30 @@ import UIKit import SnapKit -class IssueTableFooterView: UIView { +final class IssueTableFooterView: UIView { - var label: UILabel = { + private var label: UILabel = { var label = UILabel() label.text = "아래로 당기면 검색바가 보여요!👀" label.textColor = .lightGray return label }() - + override init(frame: CGRect) { super.init(frame: frame) backgroundColor = #colorLiteral(red: 0.9489405751, green: 0.9490727782, blue: 0.9685038924, alpha: 1) addSubview(label) setupAutolayout() } - + required init?(coder: NSCoder) { super.init(coder: coder) backgroundColor = #colorLiteral(red: 0.9489405751, green: 0.9490727782, blue: 0.9685038924, alpha: 1) addSubview(label) setupAutolayout() } - - func setupAutolayout() { + + private func setupAutolayout() { label.snp.makeConstraints { label in label.centerX.equalToSuperview() label.top.equalTo(39) diff --git a/iOS/issue-tracker/issue-tracker/Controller/IssueToolbar.swift b/iOS/issue-tracker/issue-tracker/Controller/IssueToolbar.swift index 13e085548..c5b40c6e9 100644 --- a/iOS/issue-tracker/issue-tracker/Controller/IssueToolbar.swift +++ b/iOS/issue-tracker/issue-tracker/Controller/IssueToolbar.swift @@ -7,32 +7,32 @@ import UIKit -class IssueToolbar: UIToolbar { - - let checkBoxBarButtonItem: UIBarButtonItem = { +final class IssueToolbar: UIToolbar { + + private(set) var checkBoxBarButtonItem: UIBarButtonItem = { var item = UIBarButtonItem() item.image = UIImage(systemName: "checkmark.circle") return item }() - - let closeIssueBarButtonItem: UIBarButtonItem = { + + private(set) var closeIssueBarButtonItem: UIBarButtonItem = { var item = UIBarButtonItem() item.image = UIImage(systemName: "archivebox") return item }() - - let labelBarButtonItem: UIBarButtonItem = { + + private(set) var labelBarButtonItem: UIBarButtonItem = { var item = UIBarButtonItem() item.title = "이슈를 선택하세요" item.isEnabled = false return item }() - - let flexibleBarButtonItem: UIBarButtonItem = { + + private let flexibleBarButtonItem: UIBarButtonItem = { var item = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) return item }() - + override init(frame: CGRect) { super.init(frame: frame) setupToolbar() @@ -42,8 +42,8 @@ class IssueToolbar: UIToolbar { super.init(coder: coder) setupToolbar() } - - func setupToolbar() { + + private func setupToolbar() { let items = [checkBoxBarButtonItem, flexibleBarButtonItem, labelBarButtonItem, flexibleBarButtonItem, closeIssueBarButtonItem] setItems(items, animated: false) } diff --git a/iOS/issue-tracker/issue-tracker/Controller/LabelViewController.swift b/iOS/issue-tracker/issue-tracker/Controller/LabelViewController.swift index e5476cd3c..d8fe8384e 100644 --- a/iOS/issue-tracker/issue-tracker/Controller/LabelViewController.swift +++ b/iOS/issue-tracker/issue-tracker/Controller/LabelViewController.swift @@ -6,51 +6,80 @@ // import UIKit +import RxSwift +import RxCocoa -struct Label { - let title: String - let description: String - let color: String -} - -class LabelViewController: UIViewController { +final class LabelViewController: UIViewController { @IBOutlet weak var labelTableView: UITableView! - - var addLabelButton = AddLabelButton() - - let fakeData = [Label(title: "hidsfadsfsafasfdfadsf", description: "hello", color: "#B1CAE5"), Label(title: "wow", description: "amazing", color: "#DFCD85")] - + + private var labelListViewModel = LabelListViewModel(networkManager: NetworkManager()) + private var addLabelButton = AddLabelButton() + private var bag = DisposeBag() + override func viewDidLoad() { super.viewDidLoad() seuptNavigationBar() labelTableView.register(LabelTableViewCell.self, forCellReuseIdentifier: LabelTableViewCell.identifier) - labelTableView.dataSource = self + labelTableView.separatorStyle = .none addLabelButton.addTarget(self, action: #selector(addLabelButtonTapped), for: .touchUpInside) + bindTableView() + labelTableView.delegate = self } - - @objc func addLabelButtonTapped() { - + + private func fetchLabel() { + labelListViewModel.fetchLabelList() + } + + private func bindTableView() { + labelListViewModel.labelList.bind(to: labelTableView.rx.items) { tableView, _, element in + guard let cell = tableView.dequeueReusableCell(withIdentifier: LabelTableViewCell.identifier) as? LabelTableViewCell else { return UITableViewCell()} + cell.setupLabelCell(title: element.title, description: element.description!, color: element.color) + cell.contentView.backgroundColor = .systemYellow + return cell + } + .disposed(by: bag) } - - func seuptNavigationBar() { + + @objc private func addLabelButtonTapped() { + let viewController = AddLabelViewController() + viewController.delegate = self + let navigationViewController = UINavigationController(rootViewController: viewController) + viewController.reloadDataHandler = { [weak self] in + self?.labelListViewModel.fetchLabelList() + } + present(navigationViewController, animated: true, completion: nil) + } + + private func seuptNavigationBar() { navigationController?.navigationBar.prefersLargeTitles = true navigationItem.title = "레이블" navigationItem.rightBarButtonItem = UIBarButtonItem(customView: addLabelButton) } } -extension LabelViewController: UITableViewDataSource { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return fakeData.count +extension LabelViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + let deleteAction = UIContextualAction(style: .destructive, title: "삭제") { [weak self] _, _, success in + guard let self = self else { return } + self.labelListViewModel.deleteLabel(id: self.labelListViewModel.labelList.value[indexPath.row].id!) + success(true) + } + + let closeAction = UIContextualAction(style: .normal, title: "수정") {_, _, success in + success(true) + } + + deleteAction.image = UIImage(systemName: "trash") + closeAction.image = UIImage(systemName: "archivebox") + closeAction.backgroundColor = #colorLiteral(red: 0.7988751531, green: 0.8300203681, blue: 0.9990373254, alpha: 1) + + return UISwipeActionsConfiguration(actions: [closeAction, deleteAction]) } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: LabelTableViewCell.identifier) as? LabelTableViewCell else { return UITableViewCell() } - cell.setupLabelCell(title: fakeData[indexPath.row].title, description: fakeData[indexPath.row].description, color: fakeData[indexPath.row].color) - - return cell +} + +extension LabelViewController: AddLabelViewControllerDelegate { + func fetchData() { + fetchLabel() } - - } diff --git a/iOS/issue-tracker/issue-tracker/Controller/LoginViewController.swift b/iOS/issue-tracker/issue-tracker/Controller/LoginViewController.swift index 35587aad0..39446a8e9 100644 --- a/iOS/issue-tracker/issue-tracker/Controller/LoginViewController.swift +++ b/iOS/issue-tracker/issue-tracker/Controller/LoginViewController.swift @@ -6,10 +6,43 @@ // import UIKit +import AuthenticationServices +@IBDesignable class LoginViewController: UIViewController { - + + @IBOutlet weak var contentView: LoginView! + @IBOutlet weak var githubLoginButton: GitHubLoginButton! + + private let authManager = OAuthManager(networkManager: NetworkManager()) + + @IBAction func didTapGithubLogin(_ sender: Any) { + let session = authManager.authenticate() + session.presentationContextProvider = self + session.start() + } + override func viewDidLoad() { super.viewDidLoad() + view.backgroundColor = .systemGroupedBackground + + NotificationCenter.default.addObserver(self, selector: #selector(requestToken), name: Notification.Name.init("authorized"), object: nil) + } + + @objc func requestToken() { + authManager.requestJWTToken(completion: { + let st = UIStoryboard(name: "Main", bundle: nil) + let vc = st.instantiateViewController(withIdentifier: "main") + vc.modalPresentationStyle = .fullScreen + DispatchQueue.main.async { + self.present(vc, animated: true) + } + }) + } +} + +extension LoginViewController: ASWebAuthenticationPresentationContextProviding { + func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + return self.view.window ?? ASPresentationAnchor() } } diff --git a/iOS/issue-tracker/issue-tracker/Controller/MilestoneViewController.swift b/iOS/issue-tracker/issue-tracker/Controller/MilestoneViewController.swift index 63dce4bf4..799079fbc 100644 --- a/iOS/issue-tracker/issue-tracker/Controller/MilestoneViewController.swift +++ b/iOS/issue-tracker/issue-tracker/Controller/MilestoneViewController.swift @@ -6,55 +6,78 @@ // import UIKit +import RxSwift +import RxCocoa class MilestoneViewController: UIViewController { - + private let viewModel = MilestoneViewModel.shared + private let disposeBag = DisposeBag() private let tableView: UITableView = { let tableView = UITableView() - tableView.rowHeight = 200 + tableView.rowHeight = 150 tableView.register(MilestoneTableViewCell.self, forCellReuseIdentifier: MilestoneTableViewCell.reuseId) + tableView.separatorStyle = .none return tableView }() - + override func viewDidLoad() { super.viewDidLoad() - tableView.dataSource = self + tableView.delegate = self tableView.frame = view.bounds + view.addSubview(tableView) - + navigationController?.navigationBar.prefersLargeTitles = true navigationItem.title = "마일스톤" navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addMilestone)) + + setupObserver() + setupCellConfiguration() } - + @objc func addMilestone() { - let nav = UINavigationController(rootViewController: AddViewController()) + let nav = UINavigationController(rootViewController: AddMilestoneViewController()) present(nav, animated: true, completion: nil) } } -extension MilestoneViewController: UITableViewDataSource { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 3 +private extension MilestoneViewController { + func setupObserver() { + viewModel + .subject + .subscribe(onNext: { [unowned self] _ in + self.tableView.reloadData() + }) + .disposed(by: disposeBag) } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: MilestoneTableViewCell.reuseId, for: indexPath) - return cell + + func setupCellConfiguration() { + viewModel + .subject + .bind(to: tableView + .rx + .items(cellIdentifier: MilestoneTableViewCell.reuseId, + cellType: MilestoneTableViewCell.self)) { _, milestone, cell in + cell.configure(with: milestone) + cell.contentView.backgroundColor = .systemYellow + } + .disposed(by: disposeBag) } } extension MilestoneViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - let delete = UIContextualAction(style: .destructive, title: "삭제", handler: { action, view, completion in + let delete = UIContextualAction(style: .destructive, title: "삭제", handler: { [unowned self] _, _, completion in + let id = viewModel.subject.value[indexPath.row].id + viewModel.delete(id: id) completion(true) }) delete.image = UIImage(systemName: "trash") - let edit = UIContextualAction(style: .normal, title: "수정", handler: { action, view, completion in + let edit = UIContextualAction(style: .normal, title: "수정", handler: { _, _, completion in completion(true) }) edit.image = UIImage(systemName: "pencil") - return UISwipeActionsConfiguration(actions:[delete, edit]) + return UISwipeActionsConfiguration(actions: [delete, edit]) } } diff --git a/iOS/issue-tracker/issue-tracker/Controller/ModalViewController.swift b/iOS/issue-tracker/issue-tracker/Controller/ModalViewController.swift index b9b71146d..ca9dbeb7d 100644 --- a/iOS/issue-tracker/issue-tracker/Controller/ModalViewController.swift +++ b/iOS/issue-tracker/issue-tracker/Controller/ModalViewController.swift @@ -12,14 +12,14 @@ class ModalViewController: UIViewController { @IBOutlet weak var tableView: UITableView! private let labelText = ["레이블", "마일스톤", "이슈 편집", "이슈 닫기", "열린 이슈"] private let supplementary = ["document", "없음", "pencil", "archivebox", "trash"] - + override func viewDidLoad() { super.viewDidLoad() view.layer.cornerRadius = 15 setupGestureRecognizers() } - + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() let height: CGFloat = 353 + bottomSafeAreaHeight @@ -42,12 +42,15 @@ private extension ModalViewController { } extension ModalViewController: UITableViewDataSource, UITableViewDelegate { + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return "이슈 상세 정보" + } + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return labelText.count } - + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) cell.textLabel?.text = labelText[indexPath.row] switch indexPath.row { @@ -66,7 +69,7 @@ class DetailTextStyleTableViewCell: UITableViewCell { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: .value1, reuseIdentifier: reuseIdentifier) } - + required init?(coder: NSCoder) { super.init(coder: coder) } diff --git a/iOS/issue-tracker/issue-tracker/Controller/NewIssueViewController.swift b/iOS/issue-tracker/issue-tracker/Controller/NewIssueViewController.swift index f0d7c1f11..0e59bf6e0 100644 --- a/iOS/issue-tracker/issue-tracker/Controller/NewIssueViewController.swift +++ b/iOS/issue-tracker/issue-tracker/Controller/NewIssueViewController.swift @@ -8,87 +8,137 @@ import UIKit import SnapKit import MarkdownKit +import RxSwift + +protocol NewIssueViewDelegate: AnyObject { + func refresh() +} -@IBDesignable class NewIssueViewController: UIViewController { - - private let additionalInfo = ["레이블", "마일스톤", "작성자"] + private let additionalInfo = ["레이블", "마일스톤", "담당자"] private let cellReuseIdentifier = "NewIssueViewCell" private let markdownParser = MarkdownParser() - + private var viewModel = NewIssueViewModel() + private let disposeBag = DisposeBag() + weak var delegate: NewIssueViewDelegate? + private let subject: UILabel = { let label = UILabel() label.text = "제목" return label }() - - private let textField: UITextField = { + + private let subjectTextField: UITextField = { let textField = UITextField() + textField.becomeFirstResponder() textField.placeholder = "제목을 입력하세요." return textField }() - - private lazy var tableView: UITableView = { + + private let tableView: UITableView = { let tableView = UITableView() + tableView.rowHeight = 44.0 tableView.isScrollEnabled = false - tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellReuseIdentifier) + tableView.register(DetailTextStyleTableViewCell.self, forCellReuseIdentifier: "NewIssueViewCell") return tableView }() + private let saveButton = UIBarButtonItem(title: "저장", style: .plain, target: self, action: #selector(didTapSave)) private let segmentedControl = UISegmentedControl(items: ["마크다운", "미리보기"]) - private var textView: UITextView? + private let contentTextView: UITextView = { + let textView = UITextView() + textView.text = "코멘트는 여기에 입력하세요." + return textView + }() + private var textString: String? - + override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground - + tableView.dataSource = self tableView.delegate = self - - let save = UIBarButtonItem(title: "저장", style: .plain, target: self, action: nil) - navigationItem.rightBarButtonItem = save + + navigationItem.largeTitleDisplayMode = .never + navigationItem.rightBarButtonItem = saveButton navigationItem.titleView = segmentedControl + segmentedControl.selectedSegmentIndex = 0 segmentedControl.addTarget(self, action: #selector(reload), for: .valueChanged) - + view.addSubview(subject) - view.addSubview(textField) + view.addSubview(subjectTextField) + view.addSubview(contentTextView) view.addSubview(tableView) + + setupAutolayout() + bind() } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - tabBarController?.tabBar.isHidden = true + + private func bind() { + subjectTextField.rx.text + .orEmpty + .distinctUntilChanged() + .bind(onNext: { [unowned self] title in + viewModel.title = title + }) + .disposed(by: disposeBag) + + contentTextView.rx.text + .orEmpty + .distinctUntilChanged() + .bind(onNext: { [unowned self] text in + viewModel.content = text + }) + .disposed(by: disposeBag) } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() + + private func setupAutolayout() { subject.snp.makeConstraints { maker in maker.leading.top.equalTo(view.safeAreaLayoutGuide).inset(20) maker.width.equalTo(70) } - textField.snp.makeConstraints { maker in + subjectTextField.snp.makeConstraints { maker in maker.leading.equalTo(subject.snp.trailing) maker.trailing.equalTo(view.safeAreaLayoutGuide).inset(20) maker.height.equalTo(44) maker.centerY.equalTo(subject.snp.centerY) } - tableView.snp.makeConstraints { maker in - maker.top.equalTo(textField.snp.bottom) + contentTextView.snp.makeConstraints { maker in + maker.top.equalTo(subjectTextField.snp.bottom) maker.leading.trailing.equalTo(view.safeAreaLayoutGuide).inset(20) + maker.height.equalTo(440) + } + tableView.snp.makeConstraints { maker in + maker.top.equalTo(contentTextView.snp.bottom) + maker.leading.trailing.equalTo(view.safeAreaLayoutGuide) maker.bottom.equalTo(view.safeAreaLayoutGuide) } } - + + @objc + func didTapSave(sender: UIButton) { + guard let title = subjectTextField.text, let content = contentTextView.text, !title.isEmpty, !content.isEmpty else { + let alert = UIAlertController(title: "Oops!", message: "제목과 내용을 모두 입력해주세요!", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "확인", style: .default)) + self.present(alert, animated: true) + return + } + viewModel.post { [weak self] in + self?.delegate?.refresh() + self?.navigationController?.popViewController(animated: true) + } + } + @objc func reload(sender: UISegmentedControl) { switch sender.selectedSegmentIndex { case 0: - textView?.text = textString + contentTextView.text = textString case 1: - textString = textView?.text - textView?.attributedText = markdownParser.parse(textView!.text) + textString = contentTextView.text + contentTextView.attributedText = markdownParser.parse(contentTextView.text) default: break } @@ -96,57 +146,34 @@ class NewIssueViewController: UIViewController { } extension NewIssueViewController: UITableViewDataSource { - func numberOfSections(in tableView: UITableView) -> Int { - return 2 - } - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - switch section { - case 0: - return 1 - case 1: - return 3 - default: - return 0 - } + return 3 } - + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath) cell.selectionStyle = .none - switch indexPath.section { - case 0: - textView = UITextView(frame: cell.contentView.bounds) - cell.addSubview(textView!) - case 1: - cell.textLabel?.text = additionalInfo[indexPath.row] - cell.accessoryType = .disclosureIndicator - default: - break - } + cell.textLabel?.text = additionalInfo[indexPath.row] + cell.accessoryType = .disclosureIndicator return cell } } extension NewIssueViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - if section == 1 { - return "추가 정보" + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let vc = AdditionalInfoViewController() + vc.setupSelectedData = { data in + let cell = tableView.cellForRow(at: indexPath) + cell?.detailTextLabel?.text = data } - return nil + let nav = UINavigationController(rootViewController: vc) + present(nav, animated: true) } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - switch indexPath.section { - case 0: - return 440 - case 1: - return 44 - default: - return 0 - } + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return "추가 정보" } - + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { return UIView() } diff --git a/iOS/issue-tracker/issue-tracker/Extension/UIColor+HexInit.swift b/iOS/issue-tracker/issue-tracker/Extension/UIColor+HexInit.swift index 92a021f07..ee8604310 100644 --- a/iOS/issue-tracker/issue-tracker/Extension/UIColor+HexInit.swift +++ b/iOS/issue-tracker/issue-tracker/Extension/UIColor+HexInit.swift @@ -8,18 +8,18 @@ import UIKit extension UIColor { - static func hexStringToUIColor (hex:String) -> UIColor { - var cString:String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() + static func hexStringToUIColor(hex: String) -> UIColor { + var cString: String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() - if (cString.hasPrefix("#")) { + if cString.hasPrefix("#") { cString.remove(at: cString.startIndex) } - if ((cString.count) != 6) { + if (cString.count) != 6 { return UIColor.gray } - var rgbValue:UInt64 = 0 + var rgbValue: UInt64 = 0 Scanner(string: cString).scanHexInt64(&rgbValue) return UIColor( @@ -29,4 +29,29 @@ extension UIColor { alpha: CGFloat(1.0) ) } + + func convertHexToString() -> String { + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + + getRed(&red, green: &green, blue: &blue, alpha: &alpha) + + let rgb: Int = (Int)(red*255)<<16 | (Int)(green*255)<<8 | (Int)(blue*255)<<0 + + return NSString(format: "#%06x", rgb) as String + } + + static func getRandomColor() -> UIColor { + + let randomRed: CGFloat = CGFloat(drand48()) + + let randomGreen: CGFloat = CGFloat(drand48()) + + let randomBlue: CGFloat = CGFloat(drand48()) + + return UIColor(red: randomRed, green: randomGreen, blue: randomBlue, alpha: 1.0) + + } } diff --git a/iOS/issue-tracker/issue-tracker/Extension/UIKit+Extension.swift b/iOS/issue-tracker/issue-tracker/Extension/UIKit+Extension.swift index 9b08b7390..e343bb159 100644 --- a/iOS/issue-tracker/issue-tracker/Extension/UIKit+Extension.swift +++ b/iOS/issue-tracker/issue-tracker/Extension/UIKit+Extension.swift @@ -12,11 +12,11 @@ extension UIViewController { guard let statusBarHeight = view.window?.windowScene?.statusBarManager?.statusBarFrame.height else { return 0 } return statusBarHeight } - + var topBarHeight: CGFloat { return statusBarHeight + (navigationController?.navigationBar.frame.height ?? 0) } - + var bottomSafeAreaHeight: CGFloat { return view.safeAreaInsets.bottom } diff --git a/iOS/issue-tracker/issue-tracker/Info.plist b/iOS/issue-tracker/issue-tracker/Info.plist index 5b531f7b2..53c6efd04 100644 --- a/iOS/issue-tracker/issue-tracker/Info.plist +++ b/iOS/issue-tracker/issue-tracker/Info.plist @@ -2,6 +2,11 @@ + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable diff --git a/iOS/issue-tracker/issue-tracker/Login.storyboard b/iOS/issue-tracker/issue-tracker/Login.storyboard index faed6a070..a70bcaf41 100644 --- a/iOS/issue-tracker/issue-tracker/Login.storyboard +++ b/iOS/issue-tracker/issue-tracker/Login.storyboard @@ -12,31 +12,52 @@ - + - + + - - - - + + + + + + + + + + + + + + + + diff --git a/iOS/issue-tracker/issue-tracker/Model/Endpoint.swift b/iOS/issue-tracker/issue-tracker/Model/Endpoint.swift index 76599bccc..c9c867d7a 100644 --- a/iOS/issue-tracker/issue-tracker/Model/Endpoint.swift +++ b/iOS/issue-tracker/issue-tracker/Model/Endpoint.swift @@ -8,23 +8,30 @@ import Foundation struct Endpoint { - let scheme: String = "https" - let host: String = "" + let scheme: String = "http" + let host: String = "3.37.161.3" let port: Int = 8080 var path: Path - func url(queryItems: [URLQueryItem] = []) -> URL? { + func url(queryItems: [URLQueryItem] = [], id: Int? = nil) -> URL? { var urlComponents = URLComponents() urlComponents.scheme = scheme urlComponents.host = host urlComponents.port = port urlComponents.path = path.pathString + if let id = id { + urlComponents.path = path.pathString + "/" + String(id) + } urlComponents.queryItems = queryItems return urlComponents.url } enum Path: String { - case login = "/login" + case login = "/api/user/login/oauth/githubios" + case label = "/api/label" + case issue = "/api/issue" + case milestone = "/api/milestone" + case user = "/api/user" var pathString: String { return self.rawValue diff --git a/iOS/issue-tracker/issue-tracker/Model/NetworkManager.swift b/iOS/issue-tracker/issue-tracker/Model/NetworkManager.swift index 91fcab113..7cbe27053 100644 --- a/iOS/issue-tracker/issue-tracker/Model/NetworkManager.swift +++ b/iOS/issue-tracker/issue-tracker/Model/NetworkManager.swift @@ -10,13 +10,25 @@ import Alamofire protocol Networkable { func request(url: URL, decodableType: T.Type, completion: @escaping (T) -> Void) + func postRequest(url: URL, encodable: T, completion: @escaping (Result) -> Void) + func deleteRequest(url: URL, completion: @escaping (Result) -> Void) + func patchRequest(url: URL, encodable: T, completion: @escaping (Result) -> Void) } -class NetworkManager { - private let httpHeaders: HTTPHeaders = ["Content-Type": "application/json", "Accept": "application/json"] +class NetworkManager: Networkable { + private lazy var httpHeaders: HTTPHeaders = ["Content-Type": "application/json", + "Accept": "application/json", + "Authorization": getToken()] + + func getToken() -> String { + guard let token = UserDefaults.standard.string(forKey: "token") else { + return "" + } + return token + } func request(url: URL, decodableType: T.Type, completion: @escaping (T) -> Void) { - AF.request(url, method: .get, encoding: URLEncoding.default, headers: httpHeaders) + AF.request(url, method: .get, headers: httpHeaders) .validate(statusCode: 200..<300) .responseDecodable(of: decodableType) { (response) in switch response.result { @@ -27,6 +39,28 @@ class NetworkManager { } } } -} + func postRequest(url: URL, encodable: T, completion: @escaping (Result) -> Void) { + AF.request(url, method: .post, parameters: encodable, encoder: JSONParameterEncoder.default, headers: httpHeaders) + .validate(statusCode: 200..<300) + .response { response in + completion(response.result) + } + } + + func deleteRequest(url: URL, completion: @escaping (Result) -> Void) { + AF.request(url, method: .delete, headers: httpHeaders) + .validate(statusCode: 200..<300) + .response { response in + completion(response.result) + } + } + func patchRequest(url: URL, encodable: T, completion: @escaping (Result) -> Void) { + AF.request(url, method: .patch, parameters: encodable, encoder: JSONParameterEncoder.default, headers: httpHeaders) + .validate(statusCode: 200..<300) + .response { response in + completion(response.result) + } + } +} diff --git a/iOS/issue-tracker/issue-tracker/Model/OAuthManager.swift b/iOS/issue-tracker/issue-tracker/Model/OAuthManager.swift index accb90fc4..d545e1aee 100644 --- a/iOS/issue-tracker/issue-tracker/Model/OAuthManager.swift +++ b/iOS/issue-tracker/issue-tracker/Model/OAuthManager.swift @@ -9,12 +9,13 @@ import Foundation import AuthenticationServices class OAuthManager { - private let cliendId = "" - private lazy var authURL = URL(string: "https://github.com/login/oauth/authorize?client_id=\(cliendId)&scope=user:email")! - private let callbackUrlScheme = "issueTracker" - + private let cliendId = "4cccb9b4007d25b53a70" + private lazy var authURL = URL(string: "https://github.com/login/oauth/authorize?client_id=\(cliendId)")! + private let callbackUrlScheme = "issue-tracker" + private var code: String? + var networkManager: Networkable - + init(networkManager: Networkable) { self.networkManager = networkManager } @@ -28,19 +29,24 @@ class OAuthManager { print("An error occurred when attempting to sign in.") return } - print(code) - self.requestJWTToken(with: code) + self.code = code + NotificationCenter.default.post(name: Notification.Name.init("authorized"), object: self) }) return authenticationSession } - - private func requestJWTToken(with code: String) { + + func requestJWTToken(completion: @escaping () -> Void) { let query = URLQueryItem(name: "code", value: code) let endpoint = Endpoint(path: .login) guard let url = endpoint.url(queryItems: [query]) else { return } - networkManager.request(url: url, decodableType: [String: String].self) { (token) in - UserDefaults.standard.set(token, forKey: "token") - // 메인 이슈 화면으로 + networkManager.request(url: url, decodableType: Token.self) { data in + let token = data.data + UserDefaults.standard.setValue(token, forKey: "token") + completion() } } } + +struct Token: Decodable { + let data: String +} diff --git a/iOS/issue-tracker/issue-tracker/Model/UserDTO/Comment.swift b/iOS/issue-tracker/issue-tracker/Model/UserDTO/Comment.swift index fd0e48500..6acb4fc15 100644 --- a/iOS/issue-tracker/issue-tracker/Model/UserDTO/Comment.swift +++ b/iOS/issue-tracker/issue-tracker/Model/UserDTO/Comment.swift @@ -21,3 +21,8 @@ struct Comment: Codable { case content } } + +struct PostComment: Codable { + let writer: String + let comment: String +} diff --git a/iOS/issue-tracker/issue-tracker/Model/UserDTO/IssueList.swift b/iOS/issue-tracker/issue-tracker/Model/UserDTO/IssueList.swift index 741bd2237..120d9fbe8 100644 --- a/iOS/issue-tracker/issue-tracker/Model/UserDTO/IssueList.swift +++ b/iOS/issue-tracker/issue-tracker/Model/UserDTO/IssueList.swift @@ -18,25 +18,29 @@ struct IssueDetail: Codable { } // MARK: - Issue -struct Issue: Codable { +struct Issue: Codable, Equatable { + static func == (lhs: Issue, rhs: Issue) -> Bool { + return lhs.id == rhs.id + } + let id: Int? let title: String - let issueNumber: Int - let isOpen: Bool + let number: Int + let open: Bool let createdTime: String let author: Author - let label: [IssueLabel] + let labels: [IssueLabel]? let assignee: [Author]? - let milestone: Milestone + let milestone: Milestone? let comment: [Comment]? enum CodingKeys: String, CodingKey { case id case title - case issueNumber = "issue_number" - case isOpen + case number + case open case createdTime = "created_time" - case author, label, assignee, milestone, comment + case author, labels, assignee, milestone, comment } } @@ -54,7 +58,7 @@ struct Author: Codable { // MARK: - IssueLabel struct IssueLabel: Codable { - let id: Int + let id: Int? let title, color: String let fontColor: String? let description: String? diff --git a/iOS/issue-tracker/issue-tracker/Model/UserDTO/LabelList.swift b/iOS/issue-tracker/issue-tracker/Model/UserDTO/LabelList.swift new file mode 100644 index 000000000..2e1640613 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Model/UserDTO/LabelList.swift @@ -0,0 +1,12 @@ +// +// LabelList.swift +// issue-tracker +// +// Created by 양준혁 on 2021/06/15. +// + +import Foundation + +struct LabelList: Decodable { + let data: [IssueLabel] +} diff --git a/iOS/issue-tracker/issue-tracker/Model/UserDTO/MilestoneList.swift b/iOS/issue-tracker/issue-tracker/Model/UserDTO/MilestoneList.swift index 7b667151c..ef5a0984d 100644 --- a/iOS/issue-tracker/issue-tracker/Model/UserDTO/MilestoneList.swift +++ b/iOS/issue-tracker/issue-tracker/Model/UserDTO/MilestoneList.swift @@ -7,8 +7,12 @@ import Foundation -struct MilestoneList { - let milestone: [Milestone] +struct MilestoneList: Codable { + let data: [Milestone] + + enum CodingKeys: String, CodingKey { + case data + } } // MARK: - Milestone @@ -19,11 +23,12 @@ struct Milestone: Codable { let createdTime: String? let dueDate: String? let closedIssueCount, openedIssueCount: Int? - + enum CodingKeys: String, CodingKey { - case id, title, description, closedIssueCount, openedIssueCount + case id, title, description case createdTime = "created_time" case dueDate = "due_date" - + case closedIssueCount = "closed_issue_count" + case openedIssueCount = "opened_issue_count" } } diff --git a/iOS/issue-tracker/issue-tracker/Model/UserDTO/NewIssue.swift b/iOS/issue-tracker/issue-tracker/Model/UserDTO/NewIssue.swift index c5e3654f4..d6a3f5a49 100644 --- a/iOS/issue-tracker/issue-tracker/Model/UserDTO/NewIssue.swift +++ b/iOS/issue-tracker/issue-tracker/Model/UserDTO/NewIssue.swift @@ -7,16 +7,16 @@ import Foundation -struct NewIssue { - let title: String +struct NewIssue: Codable { + var title: String let comment: String - let labelsID: [Int] - let milestoneID: Int - let assigneeID: [Int] - + let labelID: [Int] = [] + let milestoneID: Int? = nil + let assigneeID: [Int] = [] + enum CodingKeys: String, CodingKey { case title, comment - case labelsID = "labels_ids" + case labelID = "label_ids" case milestoneID = "milestone_id" case assigneeID = "assignee_id" } diff --git a/iOS/issue-tracker/issue-tracker/Model/UserDTO/PatchIssue.swift b/iOS/issue-tracker/issue-tracker/Model/UserDTO/PatchIssue.swift new file mode 100644 index 000000000..a2b18d049 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Model/UserDTO/PatchIssue.swift @@ -0,0 +1,18 @@ +// +// PatchIssue.swift +// issue-tracker +// +// Created by 양준혁 on 2021/06/22. +// + +import Foundation + +struct PatchIssue: Encodable { + let issueNumber: [Int] + let isOpen: Bool + + enum CodingKeys: String, CodingKey { + case issueNumber = "issue_ids" + case isOpen = "is_open" + } +} diff --git a/iOS/issue-tracker/issue-tracker/Model/UserDTO/UserList.swift b/iOS/issue-tracker/issue-tracker/Model/UserDTO/UserList.swift index 87c5deea6..ebbbb1ccd 100644 --- a/iOS/issue-tracker/issue-tracker/Model/UserDTO/UserList.swift +++ b/iOS/issue-tracker/issue-tracker/Model/UserDTO/UserList.swift @@ -15,7 +15,7 @@ struct User: Decodable { let id: Int let name: String let imageUrl: String - + enum CodingKeys: String, CodingKey { case id, name case imageUrl = "image_url" diff --git a/iOS/issue-tracker/issue-tracker/Supporting Files/AppDelegate.swift b/iOS/issue-tracker/issue-tracker/Supporting Files/AppDelegate.swift index 426cd8038..74efe1035 100644 --- a/iOS/issue-tracker/issue-tracker/Supporting Files/AppDelegate.swift +++ b/iOS/issue-tracker/issue-tracker/Supporting Files/AppDelegate.swift @@ -9,9 +9,8 @@ import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { return true } } - diff --git a/iOS/issue-tracker/issue-tracker/Supporting Files/Assets.xcassets/failure.imageset/Contents.json b/iOS/issue-tracker/issue-tracker/Supporting Files/Assets.xcassets/failure.imageset/Contents.json new file mode 100644 index 000000000..14be3e819 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Supporting Files/Assets.xcassets/failure.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "redIcon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/issue-tracker/issue-tracker/Supporting Files/Assets.xcassets/failure.imageset/redIcon.png b/iOS/issue-tracker/issue-tracker/Supporting Files/Assets.xcassets/failure.imageset/redIcon.png new file mode 100644 index 000000000..9ca103a2e Binary files /dev/null and b/iOS/issue-tracker/issue-tracker/Supporting Files/Assets.xcassets/failure.imageset/redIcon.png differ diff --git a/iOS/issue-tracker/issue-tracker/Supporting Files/Assets.xcassets/success.imageset/Contents.json b/iOS/issue-tracker/issue-tracker/Supporting Files/Assets.xcassets/success.imageset/Contents.json new file mode 100644 index 000000000..94fb84a5a --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Supporting Files/Assets.xcassets/success.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "greenIcon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/issue-tracker/issue-tracker/Supporting Files/Assets.xcassets/success.imageset/greenIcon.png b/iOS/issue-tracker/issue-tracker/Supporting Files/Assets.xcassets/success.imageset/greenIcon.png new file mode 100644 index 000000000..73f6b6344 Binary files /dev/null and b/iOS/issue-tracker/issue-tracker/Supporting Files/Assets.xcassets/success.imageset/greenIcon.png differ diff --git a/iOS/issue-tracker/issue-tracker/Supporting Files/SceneDelegate.swift b/iOS/issue-tracker/issue-tracker/Supporting Files/SceneDelegate.swift index be86411a9..1b27288c0 100644 --- a/iOS/issue-tracker/issue-tracker/Supporting Files/SceneDelegate.swift +++ b/iOS/issue-tracker/issue-tracker/Supporting Files/SceneDelegate.swift @@ -12,7 +12,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - guard let _ = (scene as? UIWindowScene) else { return } + guard let windowScene = scene as? UIWindowScene else { return } + window = UIWindow(windowScene: windowScene) + let storyboard = UIStoryboard(name: "Login", bundle: nil) + let vc = storyboard.instantiateViewController(withIdentifier: "LoginViewController") + window?.rootViewController = vc } } - diff --git a/iOS/issue-tracker/issue-tracker/View/Alert/CustomAlertView.swift b/iOS/issue-tracker/issue-tracker/View/Alert/CustomAlertView.swift new file mode 100644 index 000000000..c6ed7588a --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/View/Alert/CustomAlertView.swift @@ -0,0 +1,160 @@ +// +// PositiveAlertView.swift +// issue-tracker +// +// Created by 양준혁 on 2021/06/23. +// + +import UIKit +import SnapKit + +final class CustomAlertView: UIView { + + static let shared = CustomAlertView() + + private var contentView: ContentView = { + var view = ContentView() + return view + }() + + private var contentImage: UIImageView = { + var imageView = UIImageView() + imageView.image = UIImage(named: "success") + imageView.layer.masksToBounds = true + imageView.layer.borderWidth = 2 + imageView.layer.borderColor = UIColor.white.cgColor + imageView.layer.cornerRadius = 40 + return imageView + }() + + private var alertType: AlertType = .failure + private var handler: (() -> Void)? + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .clear + addSubview(contentView) + addSubview(contentImage) + setUpAutoLayout() + contentView.button.addTarget(self, action: #selector(dismiss), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + backgroundColor = .clear + addSubview(contentView) + addSubview(contentImage) + setUpAutoLayout() + contentView.button.addTarget(self, action: #selector(dismiss), for: .touchUpInside) + } + + private func setUpAutoLayout() { + contentView.snp.makeConstraints { view in + view.centerX.centerY.equalToSuperview() + view.leading.trailing.equalToSuperview().inset(50) + view.height.equalTo(250) + } + contentImage.snp.makeConstraints { image in + image.centerY.equalTo(contentView.snp.top) + image.centerX.equalToSuperview() + image.height.width.equalTo(80) + } + } + + func setUpAlertView(title: String, message: String, buttonTitle: String, alertType: AlertType, buttonHandler: (() -> Void)?) { + self.frame = UIScreen.main.bounds + switch alertType { + case .success: + contentImage.image = UIImage(named: "success") + contentView.button.backgroundColor = .systemGreen + case .failure: + contentImage.image = UIImage(named: "failure") + contentView.button.backgroundColor = .red + } + contentView.titleLabel.text = title + contentView.descriptionLabel.text = message + contentView.button.setTitle(buttonTitle, for: .normal) + + UIApplication.shared.connectedScenes + .filter({$0.activationState == .foregroundActive}) + .map({$0 as? UIWindowScene}) + .compactMap({$0}) + .first?.windows + .filter({$0.isKeyWindow}).first? + .addSubview(self) + + handler = buttonHandler + } + + @objc private func dismiss() { + self.removeFromSuperview() + handler?() + } +} + +enum AlertType { + case success + case failure +} + +final class ContentView: UIView { + var titleLabel: UILabel = { + var label = UILabel() + label.font = UIFont.boldSystemFont(ofSize: 26) + label.textAlignment = .center + return label + }() + + var descriptionLabel: UILabel = { + var label = UILabel() + label.font = UIFont.systemFont(ofSize: 16) + label.numberOfLines = 2 + label.textAlignment = .center + return label + }() + + var button: UIButton = { + var button = UIButton() + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + addSubview(titleLabel) + addSubview(descriptionLabel) + addSubview(button) + backgroundColor = .white + layer.masksToBounds = true + layer.cornerRadius = 20 + setUpAutoLayout() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + addSubview(titleLabel) + addSubview(descriptionLabel) + addSubview(button) + backgroundColor = .white + layer.masksToBounds = true + layer.cornerRadius = 20 + setUpAutoLayout() + } + + private func setUpAutoLayout() { + titleLabel.snp.makeConstraints { title in + title.top.equalToSuperview().inset(60) + title.leading.trailing.equalToSuperview().inset(10) + title.height.equalTo(30) + } + descriptionLabel.snp.makeConstraints { label in + label.top.equalTo(titleLabel.snp.bottom).offset(20) + label.leading.trailing.equalToSuperview().inset(10) + label.height.equalTo(60) + } + button.snp.makeConstraints { button in + button.bottom.equalToSuperview().inset(10) + button.leading.trailing.equalToSuperview().inset(10) + button.height.equalTo(50) + } + } +} diff --git a/iOS/issue-tracker/issue-tracker/View/IssueList/AddIssueButton.swift b/iOS/issue-tracker/issue-tracker/View/IssueList/AddIssueButton.swift index 6f3c2d992..016ac585d 100644 --- a/iOS/issue-tracker/issue-tracker/View/IssueList/AddIssueButton.swift +++ b/iOS/issue-tracker/issue-tracker/View/IssueList/AddIssueButton.swift @@ -7,7 +7,7 @@ import UIKit -class AddIssueButton: UIView { +final class AddIssueButton: UIView { override init(frame: CGRect) { super.init(frame: frame) @@ -18,10 +18,10 @@ class AddIssueButton: UIView { super.init(coder: coder) setupButton() } - + override func draw(_ rect: CGRect) { let path = UIBezierPath() - + path.move(to: CGPoint(x: self.bounds.width * 0.25, y: self.bounds.height * 0.5)) path.addLine(to: CGPoint(x: self.bounds.width * 0.75, y: self.bounds.height * 0.5)) path.move(to: CGPoint(x: self.bounds.width * 0.5, y: self.bounds.height * 0.25)) @@ -29,8 +29,8 @@ class AddIssueButton: UIView { UIColor.white.set() path.stroke() } - - func setupButton() { + + private func setupButton() { clipsToBounds = true layer.cornerRadius = self.bounds.size.width * 0.5 backgroundColor = .systemBlue diff --git a/iOS/issue-tracker/issue-tracker/View/IssueList/CancelButton.swift b/iOS/issue-tracker/issue-tracker/View/IssueList/CancelButton.swift index 628731a40..d16b376ad 100644 --- a/iOS/issue-tracker/issue-tracker/View/IssueList/CancelButton.swift +++ b/iOS/issue-tracker/issue-tracker/View/IssueList/CancelButton.swift @@ -7,13 +7,13 @@ import UIKit -class CancelButton: UIButton { +final class CancelButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) setTitle("취소", for: .normal) setTitleColor(.systemBlue, for: .normal) } - + required init?(coder: NSCoder) { super.init(coder: coder) setTitle("취소", for: .normal) diff --git a/iOS/issue-tracker/issue-tracker/View/IssueList/FilterBarButton.swift b/iOS/issue-tracker/issue-tracker/View/IssueList/FilterBarButton.swift index 74ac0a4a7..295f4cb5f 100644 --- a/iOS/issue-tracker/issue-tracker/View/IssueList/FilterBarButton.swift +++ b/iOS/issue-tracker/issue-tracker/View/IssueList/FilterBarButton.swift @@ -7,14 +7,14 @@ import UIKit -class FilterBarButton: UIButton { +final class FilterBarButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) setImage(UIImage(systemName: "doc.text.magnifyingglass"), for: .normal) setTitle("필터", for: .normal) setTitleColor(.systemBlue, for: .normal) } - + required init?(coder: NSCoder) { super.init(coder: coder) setImage(UIImage(systemName: "doc.text.magnifyingglass"), for: .normal) diff --git a/iOS/issue-tracker/issue-tracker/View/IssueDetailTableViewCell.swift b/iOS/issue-tracker/issue-tracker/View/IssueList/IssueDetailTableViewCell.swift similarity index 83% rename from iOS/issue-tracker/issue-tracker/View/IssueDetailTableViewCell.swift rename to iOS/issue-tracker/issue-tracker/View/IssueList/IssueDetailTableViewCell.swift index 83a554540..5cc73976d 100644 --- a/iOS/issue-tracker/issue-tracker/View/IssueDetailTableViewCell.swift +++ b/iOS/issue-tracker/issue-tracker/View/IssueList/IssueDetailTableViewCell.swift @@ -18,7 +18,7 @@ class IssueDetailTableViewCell: UITableViewCell { stackViw.spacing = 10 return stackViw }() - + private let verticalStackView: UIStackView = { let stackViw = UIStackView() stackViw.axis = .vertical @@ -26,23 +26,22 @@ class IssueDetailTableViewCell: UITableViewCell { stackViw.distribution = .fill return stackViw }() - + private let profile: UIImageView = { let imageView = UIImageView() - imageView.image = UIImage(systemName: "bell") imageView.contentMode = .scaleAspectFit imageView.layer.masksToBounds = true imageView.layer.cornerRadius = imageView.frame.width / 2 return imageView }() - + private let author: UILabel = { let label = UILabel() label.text = "Oni" label.textColor = .label return label }() - + private let timestamp: UILabel = { let label = UILabel() label.text = "1분 전" @@ -50,7 +49,7 @@ class IssueDetailTableViewCell: UITableViewCell { label.font = .systemFont(ofSize: 14) return label }() - + private let comment: UILabel = { let label = UILabel() label.numberOfLines = 0 @@ -58,7 +57,7 @@ class IssueDetailTableViewCell: UITableViewCell { label.textColor = .label return label }() - + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) self.selectionStyle = .gray @@ -69,11 +68,11 @@ class IssueDetailTableViewCell: UITableViewCell { horizenStackView.addArrangedSubview(verticalStackView) contentView.addSubview(horizenStackView) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func layoutSubviews() { super.layoutSubviews() profile.snp.makeConstraints { maker in @@ -83,4 +82,19 @@ class IssueDetailTableViewCell: UITableViewCell { maker.leading.trailing.top.bottom.equalToSuperview().inset(20) } } + + func configure(model: Comment) { + setupProfile(url: model.author.imageURL) + author.text = model.author.name + comment.text = model.content + } + + private func setupProfile(url: String) { + DispatchQueue.global().sync { + guard let imageData = try? Data(contentsOf: URL(string: url)!) else { return } + DispatchQueue.main.async { + self.profile.image = UIImage(data: imageData) + } + } + } } diff --git a/iOS/issue-tracker/issue-tracker/View/IssueList/IssueTableViewCell.swift b/iOS/issue-tracker/issue-tracker/View/IssueList/IssueTableViewCell.swift index 45718650e..e56f157e4 100644 --- a/iOS/issue-tracker/issue-tracker/View/IssueList/IssueTableViewCell.swift +++ b/iOS/issue-tracker/issue-tracker/View/IssueList/IssueTableViewCell.swift @@ -7,125 +7,147 @@ import UIKit import SnapKit +import RxSwift +import RxCocoa + +final class IssueTableViewCell: UITableViewCell { -class IssueTableViewCell: UITableViewCell { - static var identifier = "IssueTableViewCell" - - var fakeData = [IssueLabels(title: "gdsfaewqeqwrqw2ewqweq", color: "#DFCD85"), IssueLabels(title: "gdsfa", color: "#DFCD85"), IssueLabels(title: "gdsfa", color: "#DFCD85"), IssueLabels(title: "gdsfaewqeqwrqw2ewqweq", color: "#DFCD85"), IssueLabels(title: "gdsfaewqeqwrqw2ewqweq", color: "#DFCD85")] - - var largeTitle: UILabel = { + + private var bag = DisposeBag() + + var stackView: UIStackView = { + var stackView = UIStackView() + stackView.alignment = .leading + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 10 + return stackView + }() + + private var largeTitle: UILabel = { var label = UILabel() label.font = UIFont.boldSystemFont(ofSize: 22) return label }() - - var labelDescription: UILabel = { + + private var labelDescription: UILabel = { var label = UILabel() label.textColor = .lightGray return label }() - - var milestoneView: MilestoneView = { + + private var milestoneView: MilestoneView = { var milestone = MilestoneView() return milestone }() - - var labelsCollectionView: LabelsCollectionView = { - var collectionView = LabelsCollectionView() - return collectionView - }() - - var checkBoxImageView: UIImageView = { + + var labelsCollectionView = LabelsCollectionView() + + private var checkBoxImageView: UIImageView = { var imageView = UIImageView() imageView.image = UIImage(systemName: "checkmark.circle.fill") return imageView }() - + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - labelsCollectionView.dataSource = self + setUpStackView() addSubviews() setupAutolayout() checkBoxImageView.isHidden = true } - + + override func layoutSubviews() { + super.layoutSubviews() + contentView.frame = contentView.frame.inset(by: UIEdgeInsets(top: 10, left: 2, bottom: 0, right: 2)) + contentView.layer.cornerRadius = 20 + contentView.layer.borderWidth = 2 + } + required init?(coder: NSCoder) { super.init(coder: coder) - labelsCollectionView.dataSource = self + setUpStackView() addSubviews() setupAutolayout() checkBoxImageView.isHidden = true } - - func addSubviews() { - addSubview(labelsCollectionView) - addSubview(labelDescription) - addSubview(milestoneView) - addSubview(largeTitle) - addSubview(checkBoxImageView) + + private func addSubviews() { + contentView.addSubview(stackView) + contentView.addSubview(checkBoxImageView) + contentView.backgroundColor = .systemYellow } - - func setupAutolayout() { - largeTitle.snp.makeConstraints { title in - title.top.equalTo(24) - title.leading.trailing.equalTo(16) - title.height.equalTo(28) - } - - labelDescription.snp.makeConstraints { label in - label.top.equalTo(largeTitle.snp.bottom).offset(16) - label.leading.trailing.equalToSuperview().offset(16) - label.height.equalTo(22) - } - - milestoneView.snp.makeConstraints { view in - view.top.equalTo(labelDescription.snp.bottom).offset(16) - view.leading.trailing.equalTo(16) - view.height.equalTo(22) - } - - labelsCollectionView.snp.makeConstraints { view in - view.top.equalTo(milestoneView.snp.bottom).offset(16) - view.leading.trailing.equalToSuperview().inset(16) - view.bottom.equalToSuperview() + + private func setUpStackView() { + stackView.addArrangedSubview(largeTitle) + stackView.addArrangedSubview(labelDescription) + stackView.addArrangedSubview(milestoneView) + stackView.addArrangedSubview(labelsCollectionView) + } + + private func setupAutolayout() { + stackView.snp.makeConstraints { view in + view.edges.equalToSuperview().inset(10) } - + checkBoxImageView.snp.makeConstraints { image in image.top.equalToSuperview().inset(24) image.trailing.equalToSuperview().inset(16) image.width.height.equalTo(30) } } - - func setupIssueCell(title: String, description: String, milestoneTitle: String, color: String) { - self.largeTitle.text = title - self.labelDescription.text = description - self.milestoneView.setMilestoneTitle(title: milestoneTitle) + + func setUpCollectionViewAutoLayout(view: UICollectionView) { + view.snp.makeConstraints { view in + view.width.equalToSuperview() + view.height.equalTo(25) + } } - + + func setupIssueCell(title: String?, description: String?, milestoneTitle: String?, issueLabels: [IssueLabel]?, isOpen: Bool) { + if let title = title { + self.largeTitle.text = title + largeTitle.sizeToFit() + } else { + largeTitle.isHidden = true + } + if let description = description { + self.labelDescription.text = description + labelDescription.sizeToFit() + } else { + labelDescription.isHidden = true + } + if let milestone = milestoneTitle { + self.milestoneView.setMilestoneTitle(title: milestone) + milestoneView.sizeToFit() + } else { + milestoneView.isHidden = true + } + + if !isOpen { + contentView.backgroundColor = #colorLiteral(red: 0.649335742, green: 0.109275572, blue: 0.1304466426, alpha: 1) + } + self.bindLabelCollectionView(issueLabels: issueLabels) + } + + func bindLabelCollectionView(issueLabels: [IssueLabel]?) { + guard let labels = issueLabels else { return } + labelsCollectionView.dataSource = nil + self.setUpCollectionViewAutoLayout(view: labelsCollectionView) + Observable.just(labels).bind(to: self.labelsCollectionView.rx.items) { collectionView, int, issueLabel in + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: LabelsCollectionViewCell.identifiers, for: IndexPath(row: int, section: 0 )) as? LabelsCollectionViewCell else { return UICollectionViewCell() } + cell.configure(title: issueLabel.title, color: issueLabel.color) + return cell + } + .disposed(by: bag) + } + func check() { checkBoxImageView.isHidden = false } - + func uncheck() { checkBoxImageView.isHidden = true } } - -extension IssueTableViewCell: UICollectionViewDataSource { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return fakeData.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: LabelsCollectionViewCell.identifiers, for: indexPath) as? LabelsCollectionViewCell else { return UICollectionViewCell() } - cell.configure(title: fakeData[indexPath.item].title, color: fakeData[indexPath.item].color) - return cell - } -} - -struct IssueLabels { - var title: String - var color: String -} diff --git a/iOS/issue-tracker/issue-tracker/View/IssueList/LabelsCollectionView.swift b/iOS/issue-tracker/issue-tracker/View/IssueList/LabelsCollectionView.swift index 914a6c80e..a183e1670 100644 --- a/iOS/issue-tracker/issue-tracker/View/IssueList/LabelsCollectionView.swift +++ b/iOS/issue-tracker/issue-tracker/View/IssueList/LabelsCollectionView.swift @@ -7,26 +7,25 @@ import UIKit -class LabelsCollectionView: UICollectionView { - - var labelsLayout: UICollectionViewFlowLayout = { - var layout = UICollectionViewFlowLayout() +final class LabelsCollectionView: UICollectionView { + + private var labelsLayout: LeftAlignedCollectionViewFlowLayout = { + var layout = LeftAlignedCollectionViewFlowLayout() layout.scrollDirection = .vertical - layout.estimatedItemSize = CGSize(width: 84, height: 22) + layout.estimatedItemSize = CGSize(width: 60, height: 20) layout.minimumLineSpacing = 10 layout.minimumInteritemSpacing = 10 return layout }() - + override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { super.init(frame: frame, collectionViewLayout: labelsLayout) register(LabelsCollectionViewCell.self, forCellWithReuseIdentifier: LabelsCollectionViewCell.identifiers) isScrollEnabled = false - backgroundColor = .white + backgroundColor = .clear } - + required init?(coder: NSCoder) { super.init(coder: coder) } - } diff --git a/iOS/issue-tracker/issue-tracker/View/IssueList/LeftAlignedCollectionViewFlowLayout.swift b/iOS/issue-tracker/issue-tracker/View/IssueList/LeftAlignedCollectionViewFlowLayout.swift new file mode 100644 index 000000000..19c2d40a9 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/View/IssueList/LeftAlignedCollectionViewFlowLayout.swift @@ -0,0 +1,29 @@ +// +// LeftAlignedCollectionViewFlowLayout.swift +// issue-tracker +// +// Created by 양준혁 on 2021/06/23. +// + +import UIKit + +class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout { + override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + let attributes = super.layoutAttributesForElements(in: rect) + + var leftMargin = sectionInset.left + var maxY: CGFloat = -1.0 + attributes?.forEach { layoutAttribute in + if layoutAttribute.frame.origin.y >= maxY { + leftMargin = sectionInset.left + } + + layoutAttribute.frame.origin.x = leftMargin + + leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing + maxY = max(layoutAttribute.frame.maxY, maxY) + } + + return attributes + } +} diff --git a/iOS/issue-tracker/issue-tracker/View/IssueList/MilestoneView.swift b/iOS/issue-tracker/issue-tracker/View/IssueList/MilestoneView.swift index 564d4732f..6d60f8c57 100644 --- a/iOS/issue-tracker/issue-tracker/View/IssueList/MilestoneView.swift +++ b/iOS/issue-tracker/issue-tracker/View/IssueList/MilestoneView.swift @@ -8,37 +8,37 @@ import UIKit import SnapKit -class MilestoneView: UIView { - var sfsymbolImageView: UIImageView = { +final class MilestoneView: UIView { + private var sfsymbolImageView: UIImageView = { var imageView = UIImageView() imageView.image = UIImage(named: "vector") return imageView }() - - var milestoneTitle: UILabel = { + + private var milestoneTitle: UILabel = { var label = UILabel() label.textColor = .lightGray return label }() - + override init(frame: CGRect) { super.init(frame: frame) addSubviews() setupAutolayout() } - + required init?(coder: NSCoder) { super.init(coder: coder) addSubviews() setupAutolayout() } - - func addSubviews() { + + private func addSubviews() { addSubview(sfsymbolImageView) addSubview(milestoneTitle) } - - func setupAutolayout() { + + private func setupAutolayout() { sfsymbolImageView.snp.makeConstraints { imageView in imageView.top.leading.bottom.equalToSuperview() imageView.width.equalTo(sfsymbolImageView.snp.height).multipliedBy(1) @@ -49,7 +49,7 @@ class MilestoneView: UIView { label.width.greaterThanOrEqualTo(30) } } - + func setMilestoneTitle(title: String) { self.milestoneTitle.text = title } diff --git a/iOS/issue-tracker/issue-tracker/View/IssueList/SelectBarButton.swift b/iOS/issue-tracker/issue-tracker/View/IssueList/SelectBarButton.swift index 067438b88..c6f76857d 100644 --- a/iOS/issue-tracker/issue-tracker/View/IssueList/SelectBarButton.swift +++ b/iOS/issue-tracker/issue-tracker/View/IssueList/SelectBarButton.swift @@ -7,7 +7,7 @@ import UIKit -class SelectBarButton: UIButton { +final class SelectBarButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) setImage(UIImage(systemName: "checkmark.circle"), for: .normal) @@ -15,7 +15,7 @@ class SelectBarButton: UIButton { setTitleColor(.systemBlue, for: .normal) semanticContentAttribute = .forceRightToLeft } - + required init?(coder: NSCoder) { super.init(coder: coder) setImage(UIImage(systemName: "checkmark.circle"), for: .normal) diff --git a/iOS/issue-tracker/issue-tracker/View/IssueList/ToolBarTextField.swift b/iOS/issue-tracker/issue-tracker/View/IssueList/ToolBarTextField.swift index 1b65c3ce4..0de353ccc 100644 --- a/iOS/issue-tracker/issue-tracker/View/IssueList/ToolBarTextField.swift +++ b/iOS/issue-tracker/issue-tracker/View/IssueList/ToolBarTextField.swift @@ -7,25 +7,38 @@ import UIKit -class ToolBarTextField: UITextField { +protocol ToolBarTextFieldDelegate: AnyObject { + func register() +} + +final class ToolBarTextField: UITextField { - let sendButton: UIButton = { + weak var textFieldDelegate: ToolBarTextFieldDelegate? + + private let sendButton: UIButton = { let button = UIButton() + button.addTarget(self, action: #selector(didTapSend), for: .touchUpInside) button.setImage(UIImage(systemName: "arrow.up.circle.fill"), for: .normal) return button }() - + override init(frame: CGRect) { super.init(frame: frame) self.placeholder = "코멘트를 입력하세요" self.rightView = sendButton self.rightViewMode = .always - self.layer.cornerRadius = 10 + self.layer.cornerRadius = 15 self.layer.masksToBounds = true + self.backgroundColor = .systemGroupedBackground self.translatesAutoresizingMaskIntoConstraints = false } - + required init?(coder: NSCoder) { super.init(coder: coder) } + + @objc + func didTapSend() { + textFieldDelegate?.register() + } } diff --git a/iOS/issue-tracker/issue-tracker/View/IssueListViewController.swift b/iOS/issue-tracker/issue-tracker/View/IssueListViewController.swift index b7abf37f6..282f1594c 100644 --- a/iOS/issue-tracker/issue-tracker/View/IssueListViewController.swift +++ b/iOS/issue-tracker/issue-tracker/View/IssueListViewController.swift @@ -7,66 +7,206 @@ import UIKit import SnapKit +import RxSwift +import RxCocoa -class IssueListViewController: UIViewController { +final class IssueListViewController: UIViewController { @IBOutlet weak var issueTableView: UITableView! - - let addIssueButton = AddIssueButton(frame: CGRect(x: 0, y: 0, width: 64, height: 64)) - let filterBarButton = FilterBarButton() - let selectBarButton = SelectBarButton() - let cancelButton = CancelButton() - let issueToolbar = IssueToolbar(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) - let searchController: UISearchController = { + + private let addIssueButton = AddIssueButton(frame: CGRect(x: 0, y: 0, width: 64, height: 64)) + private let filterBarButton = FilterBarButton() + private let selectBarButton = SelectBarButton() + private let cancelButton = CancelButton() + private let addIssueTapGesture = UITapGestureRecognizer() + private let issueToolbar = IssueToolbar(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + private let searchController: UISearchController = { var searchController = UISearchController(searchResultsController: nil) searchController.searchBar.setImage(UIImage(systemName: "mic.fill"), for: .bookmark, state: .normal) searchController.searchBar.showsBookmarkButton = true + searchController.obscuresBackgroundDuringPresentation = false return searchController }() - + + private var issueListViewModel = IssueListViewModel(networkManager: NetworkManager()) + private let bag = DisposeBag() + override func viewDidLoad() { super.viewDidLoad() setupNavigationItem() setupIssueTableView() setupAddIssueButtonAutolayout() - filterBarButton.addTarget(self, action: #selector(filterButtonTapped), for: .touchUpInside) - selectBarButton.addTarget(self, action: #selector(selectButtonTapped), for: .touchUpInside) - addIssueButton.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(addIssueButtonTapped))) - cancelButton.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside) + addIssueButton.addGestureRecognizer(addIssueTapGesture) + bindTableViewDataSource() + bindTableViewDelegate() + bindButton() + bindSelectMode() + bindIssueToolBar() + issueListViewModel.fetchIssueList() + view.backgroundColor = .systemGroupedBackground } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = false } - - @objc func filterButtonTapped() { - let controller = UINavigationController(rootViewController: IssueFilterViewController()) - present(controller, animated: true) + + private func bindTableViewDataSource() { + issueListViewModel.issueList + .bind(to: issueTableView.rx.items) { tableView, _, issue in + guard let cell = tableView.dequeueReusableCell(withIdentifier: IssueTableViewCell.identifier) as? IssueTableViewCell else { return UITableViewCell() } + cell.backgroundColor = .systemGroupedBackground + cell.setupIssueCell(title: issue.title, description: nil, milestoneTitle: issue.milestone?.title, issueLabels: issue.labels, isOpen: issue.open) + return cell + } + .disposed(by: bag) + } + + func bindTableViewDelegate() { + issueTableView.rx.itemSelected + .bind { [weak self] indexPath in + guard let self = self, let cell = self.issueTableView.cellForRow(at: indexPath) as? IssueTableViewCell else { return } + let issue = self.issueListViewModel.issueList.value[indexPath.row] + if self.issueListViewModel.selectMode.value { + self.issueListViewModel.selectedCell.accept(self.issueListViewModel.selectedCell.value + [issue]) + cell.selectionStyle = .none + cell.check() + } else { + guard let id = issue.id else { return } + cell.selectionStyle = .none + let controller = IssueDetailViewController() + controller.fetchData(id: id) + self.navigationController?.pushViewController(controller, animated: true) + } + } + .disposed(by: bag) + + issueTableView.rx.itemDeselected + .bind { [weak self] indexPath in + guard let self = self, let cell = self.issueTableView.cellForRow(at: indexPath) as? IssueTableViewCell else { return } + if self.issueListViewModel.selectMode.value { + let deselectedIssue = self.issueListViewModel.issueList.value[indexPath.row] + var selectedIssue = self.issueListViewModel.selectedCell.value + if let index = selectedIssue.firstIndex(where: { $0 == deselectedIssue }) { + selectedIssue.remove(at: index) + self.issueListViewModel.selectedCell.accept(selectedIssue) + } + cell.uncheck() + } + } + .disposed(by: bag) + } + + func bindButton() { + selectBarButton.rx.tap + .map { true } + .bind(to: issueListViewModel.selectMode) + .disposed(by: bag) + + cancelButton.rx.tap + .map { false } + .bind(to: issueListViewModel.selectMode) + .disposed(by: bag) + + filterBarButton.rx.tap + .bind { [weak self] _ in + self?.filterButtonTapped() + } + .disposed(by: bag) + + addIssueTapGesture.rx.event + .bind(onNext: { [weak self] _ in + self?.addIssueButtonTapped() + }) + .disposed(by: bag) + } + + func bindSelectMode() { + issueListViewModel.selectMode + .subscribe { [weak self] event in + if let element = event.element, element { + self?.selectButtonTapped() + } else { + self?.cancelButtonTapped() + } + } + .disposed(by: bag) + } + + func bindIssueToolBar() { + issueListViewModel.selectedCell + .bind { [weak self] issues in + if issues.count == 0 { + self?.issueToolbar.labelBarButtonItem.title = "이슈를 선택하세요" + self?.issueToolbar.closeIssueBarButtonItem.isEnabled = false + } else { + self?.issueToolbar.labelBarButtonItem.title = "\(issues.count)개의 이슈가 선택됨" + self?.issueToolbar.closeIssueBarButtonItem.isEnabled = true + } + } + .disposed(by: bag) + + issueToolbar.checkBoxBarButtonItem.rx.tap + .bind { [weak self] _ in + guard let self = self else { return } + let rows = self.issueTableView.numberOfRows(inSection: 0) + for row in 0.. Int { - return 2 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: IssueTableViewCell.identifier) as? IssueTableViewCell else { return UITableViewCell() } - cell.setupIssueCell(title: "제목", description: "이슈에 대한 설명", milestoneTitle: "마일스톤 이름", color: "#DFCD85") - return cell + issueTableView.separatorStyle = .none } } extension IssueListViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let cell = tableView.cellForRow(at: indexPath) as? IssueTableViewCell else { return } - cell.selectionStyle = .none - cell.check() - - let controller = IssueDetailViewController() - navigationController?.pushViewController(controller, animated: true) - } - - func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { - guard let cell = tableView.cellForRow(at: indexPath) as? IssueTableViewCell else { return } - cell.uncheck() - } - func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - let deleteAction = UIContextualAction(style: .destructive, title: "삭제") { ac, view, success in + let deleteAction = UIContextualAction(style: .destructive, title: "삭제") { [weak self] _, _, success in + guard let self = self else { return } + self.issueListViewModel.deleteIssue(id: self.issueListViewModel.issueList.value[indexPath.row].id!) success(true) } - - let shareAction = UIContextualAction(style: .normal, title: "닫기") { ac, view, success in + + let closeAction = UIContextualAction(style: .normal, title: "닫기") { [weak self] _, _, success in + guard let self = self else { return } + self.issueListViewModel.patchIssue(issues: [self.issueListViewModel.issueList.value[indexPath.row]]) success(true) } - + deleteAction.image = UIImage(systemName: "trash") - shareAction.image = UIImage(systemName: "archivebox") - shareAction.backgroundColor = #colorLiteral(red: 0.7988751531, green: 0.8300203681, blue: 0.9990373254, alpha: 1) - - return UISwipeActionsConfiguration(actions: [shareAction, deleteAction]) + closeAction.image = UIImage(systemName: "archivebox") + closeAction.backgroundColor = #colorLiteral(red: 0.7988751531, green: 0.8300203681, blue: 0.9990373254, alpha: 1) + + return UISwipeActionsConfiguration(actions: [closeAction, deleteAction]) + } +} + +extension IssueListViewController: NewIssueViewDelegate { + func refresh() { + issueListViewModel.fetchIssueList() } } diff --git a/iOS/issue-tracker/issue-tracker/View/Label/AddLabelButton.swift b/iOS/issue-tracker/issue-tracker/View/Label/AddLabelButton.swift index 76b5b0e6d..9ad00bd62 100644 --- a/iOS/issue-tracker/issue-tracker/View/Label/AddLabelButton.swift +++ b/iOS/issue-tracker/issue-tracker/View/Label/AddLabelButton.swift @@ -7,7 +7,7 @@ import UIKit -class AddLabelButton: UIButton { +final class AddLabelButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) setImage(UIImage(systemName: "plus"), for: .normal) @@ -15,7 +15,7 @@ class AddLabelButton: UIButton { setTitleColor(.systemBlue, for: .normal) semanticContentAttribute = .forceRightToLeft } - + required init?(coder: NSCoder) { super.init(coder: coder) setImage(UIImage(systemName: "plus"), for: .normal) diff --git a/iOS/issue-tracker/issue-tracker/View/Label/AddLabelTableViewCell.swift b/iOS/issue-tracker/issue-tracker/View/Label/AddLabelTableViewCell.swift new file mode 100644 index 000000000..25f72462a --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/View/Label/AddLabelTableViewCell.swift @@ -0,0 +1,41 @@ +// +// AddLabelTableViewCell.swift +// issue-tracker +// +// Created by 양준혁 on 2021/06/17. +// + +import UIKit +import RxSwift + +class AddLabelTableViewCell: UITableViewCell { + + static let identifier = "AddLabelTableViewCell" + + let textField = UITextField() + let reloadButton: UIButton = { + var button = UIButton() + button.setBackgroundImage(UIImage(systemName: "gobackward"), for: .normal) + return button + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + textField.frame = CGRect(x: self.frame.origin.x + 120, y: self.frame.origin.y, width: self.frame.width - 140, height: 44) + contentView.addSubview(textField) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configureTextFieldPlaceHolder(text: String) { + self.textField.placeholder = text + } + + func configureBackgroundCellMode() { + self.textField.text = "#3DDCFF" + self.textField.rightView = reloadButton + self.textField.rightViewMode = .always + } +} diff --git a/iOS/issue-tracker/issue-tracker/View/Label/EstimatedLabelView.swift b/iOS/issue-tracker/issue-tracker/View/Label/EstimatedLabelView.swift new file mode 100644 index 000000000..ba6972892 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/View/Label/EstimatedLabelView.swift @@ -0,0 +1,52 @@ +// +// EstimatedLabelView.swift +// issue-tracker +// +// Created by 양준혁 on 2021/06/17. +// + +import UIKit +import SnapKit + +final class EstimatedLabelView: UIView { + + private var label: PaddingLabel = { + var label = PaddingLabel(withInsets: 0, 0, 10, 10) + label.text = "레이블" + label.layer.masksToBounds = true + label.layer.cornerRadius = 10 + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + addSubview(label) + self.layer.masksToBounds = true + self.layer.cornerRadius = 10 + self.backgroundColor = #colorLiteral(red: 0.9102189541, green: 0.9093225002, blue: 0.9310914278, alpha: 1) + configureLabelAutolayout() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + private func configureLabelAutolayout() { + label.snp.makeConstraints { label in + label.centerX.centerY.equalToSuperview() + } + } + + func modifyLabelTitle(title: String) { + self.label.text = title + label.sizeToFit() + } + + func setupLabelColor(color: UIColor) { + self.label.backgroundColor = color + } + + func getLabel() -> PaddingLabel { + return self.label + } +} diff --git a/iOS/issue-tracker/issue-tracker/View/Label/LabelTableViewCell.swift b/iOS/issue-tracker/issue-tracker/View/Label/LabelTableViewCell.swift index 192b0e288..20f02c3a5 100644 --- a/iOS/issue-tracker/issue-tracker/View/Label/LabelTableViewCell.swift +++ b/iOS/issue-tracker/issue-tracker/View/Label/LabelTableViewCell.swift @@ -8,11 +8,11 @@ import UIKit import SnapKit -class LabelTableViewCell: UITableViewCell { - +final class LabelTableViewCell: UITableViewCell { + static var identifier = "LabelTableViewCell" - var labelView: PaddingLabel = { + private var labelView: PaddingLabel = { var label = PaddingLabel(withInsets: 0, 0, 10, 10) label.textAlignment = .center label.textColor = .white @@ -20,44 +20,51 @@ class LabelTableViewCell: UITableViewCell { label.layer.cornerRadius = 15 return label }() - - var labelDescription: UILabel = { + + private var labelDescription: UILabel = { var label = UILabel() return label }() - + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) addSubviews() setupAutolayout() } - + required init?(coder: NSCoder) { super.init(coder: coder) addSubviews() setupAutolayout() } - - func addSubviews() { - addSubview(labelView) - addSubview(labelDescription) + + override func layoutSubviews() { + super.layoutSubviews() + contentView.frame = contentView.frame.inset(by: UIEdgeInsets(top: 10, left: 2, bottom: 0, right: 2)) + contentView.layer.cornerRadius = 20 + contentView.layer.borderWidth = 2 + } + + private func addSubviews() { + contentView.addSubview(labelView) + contentView.addSubview(labelDescription) } - - func setupAutolayout() { + + private func setupAutolayout() { labelView.snp.makeConstraints { view in view.top.equalToSuperview().offset(24) view.left.equalToSuperview().offset(16) view.width.greaterThanOrEqualTo(50) view.height.equalTo(30) } - + labelDescription.snp.makeConstraints { label in label.top.equalTo(labelView.snp.bottom).offset(16) label.leading.trailing.equalToSuperview().offset(16) label.height.equalTo(22) } } - + func setupLabelCell(title: String, description: String, color: String) { self.labelView.text = title self.labelView.backgroundColor = UIColor.hexStringToUIColor(hex: color) diff --git a/iOS/issue-tracker/issue-tracker/View/Label/PaddingLabel.swift b/iOS/issue-tracker/issue-tracker/View/Label/PaddingLabel.swift index e5d9bbda4..8178201e9 100644 --- a/iOS/issue-tracker/issue-tracker/View/Label/PaddingLabel.swift +++ b/iOS/issue-tracker/issue-tracker/View/Label/PaddingLabel.swift @@ -7,12 +7,12 @@ import UIKit -class PaddingLabel: UILabel { +final class PaddingLabel: UILabel { - var topInset: CGFloat - var bottomInset: CGFloat - var leftInset: CGFloat - var rightInset: CGFloat + private var topInset: CGFloat + private var bottomInset: CGFloat + private var leftInset: CGFloat + private var rightInset: CGFloat required init(withInsets top: CGFloat, _ bottom: CGFloat, _ left: CGFloat, _ right: CGFloat) { self.topInset = top @@ -29,18 +29,16 @@ class PaddingLabel: UILabel { self.rightInset = 0 super.init(coder: coder) } - + override func drawText(in rect: CGRect) { let insets = UIEdgeInsets(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset) super.drawText(in: rect.inset(by: insets)) } override var intrinsicContentSize: CGSize { - get { - var contentSize = super.intrinsicContentSize - contentSize.height += topInset + bottomInset - contentSize.width += leftInset + rightInset - return contentSize - } + var contentSize = super.intrinsicContentSize + contentSize.height += topInset + bottomInset + contentSize.width += leftInset + rightInset + return contentSize } } diff --git a/iOS/issue-tracker/issue-tracker/View/Login/AppleLoginButton.swift b/iOS/issue-tracker/issue-tracker/View/Login/AppleLoginButton.swift index 49be3777a..7946109fd 100644 --- a/iOS/issue-tracker/issue-tracker/View/Login/AppleLoginButton.swift +++ b/iOS/issue-tracker/issue-tracker/View/Login/AppleLoginButton.swift @@ -9,7 +9,7 @@ import UIKit import SnapKit class AppleLoginButton: UIView { - + var stackView: UIStackView = { var stackView = UIStackView() stackView.axis = .horizontal @@ -18,14 +18,14 @@ class AppleLoginButton: UIView { stackView.spacing = 2 return stackView }() - + var appleImageView: UIImageView = { var imageView = UIImageView() imageView.contentMode = .scaleAspectFill imageView.image = UIImage(named: "apple") return imageView }() - + var loginLabel: UILabel = { var label = UILabel() label.text = "Apple 계정으로 로그인" @@ -33,7 +33,7 @@ class AppleLoginButton: UIView { label.textColor = .white return label }() - + override init(frame: CGRect) { super.init(frame: frame) setGitHubLoginButton() @@ -41,7 +41,7 @@ class AppleLoginButton: UIView { self.addSubview(stackView) setAutolayout() } - + required init?(coder: NSCoder) { super.init(coder: coder) setGitHubLoginButton() @@ -49,23 +49,23 @@ class AppleLoginButton: UIView { self.addSubview(stackView) setAutolayout() } - + func setGitHubLoginButton() { self.backgroundColor = .black self.layer.masksToBounds = true self.layer.cornerRadius = 10 } - + func setStackView() { stackView.addArrangedSubview(appleImageView) stackView.addArrangedSubview(loginLabel) } - + func setAutolayout() { appleImageView.snp.makeConstraints { image in image.width.height.equalTo(50) } - + stackView.snp.makeConstraints { stackView in stackView.centerX.centerY.equalToSuperview() } diff --git a/iOS/issue-tracker/issue-tracker/View/Login/GitHubLoginButton.swift b/iOS/issue-tracker/issue-tracker/View/Login/GitHubLoginButton.swift index 0888a258b..7c1da7fd4 100644 --- a/iOS/issue-tracker/issue-tracker/View/Login/GitHubLoginButton.swift +++ b/iOS/issue-tracker/issue-tracker/View/Login/GitHubLoginButton.swift @@ -8,8 +8,9 @@ import UIKit import SnapKit -class GitHubLoginButton: UIView { - +@IBDesignable +class GitHubLoginButton: UIButton { + var stackView: UIStackView = { var stackView = UIStackView() stackView.axis = .horizontal @@ -18,14 +19,14 @@ class GitHubLoginButton: UIView { stackView.spacing = 8 return stackView }() - + var octocatImageView: UIImageView = { var imageView = UIImageView() imageView.contentMode = .scaleAspectFill imageView.image = UIImage(named: "github") return imageView }() - + var loginLabel: UILabel = { var label = UILabel() label.text = "GitHub 계정으로 로그인" @@ -33,7 +34,7 @@ class GitHubLoginButton: UIView { label.textColor = .white return label }() - + override init(frame: CGRect) { super.init(frame: frame) setGitHubLoginButton() @@ -41,7 +42,7 @@ class GitHubLoginButton: UIView { self.addSubview(stackView) setAutolayout() } - + required init?(coder: NSCoder) { super.init(coder: coder) setGitHubLoginButton() @@ -49,23 +50,23 @@ class GitHubLoginButton: UIView { self.addSubview(stackView) setAutolayout() } - + func setGitHubLoginButton() { self.backgroundColor = .black self.layer.masksToBounds = true self.layer.cornerRadius = 10 } - + func setStackView() { stackView.addArrangedSubview(octocatImageView) stackView.addArrangedSubview(loginLabel) } - + func setAutolayout() { octocatImageView.snp.makeConstraints { image in image.width.height.equalTo(30) } - + stackView.snp.makeConstraints { stackView in stackView.centerX.centerY.equalToSuperview() } diff --git a/iOS/issue-tracker/issue-tracker/View/Login/IDPasswordTextField.swift b/iOS/issue-tracker/issue-tracker/View/Login/IDPasswordTextField.swift index 1e1481880..a8319991f 100644 --- a/iOS/issue-tracker/issue-tracker/View/Login/IDPasswordTextField.swift +++ b/iOS/issue-tracker/issue-tracker/View/Login/IDPasswordTextField.swift @@ -7,54 +7,53 @@ import UIKit import SnapKit -@IBDesignable class IDPasswordTextField: UIView { - + let IDLabel: UILabel = { var label = UILabel() label.text = "아이디" return label }() - + let passwordLabel: UILabel = { var label = UILabel() label.text = "비밀번호" return label }() - + let line: UIView = { var line = UIView() line.layer.borderColor = UIColor.lightGray.cgColor line.layer.borderWidth = 1 return line }() - + let IDTextField: UITextField = { var textField = UITextField() textField.borderStyle = .none return textField }() - + let passwordTextField: UITextField = { var textField = UITextField() textField.borderStyle = .none return textField }() - + override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .white addSubViews() autoLayout() } - + required init?(coder: NSCoder) { super.init(coder: coder) backgroundColor = .white addSubViews() autoLayout() } - + func addSubViews() { self.addSubview(IDLabel) self.addSubview(passwordLabel) @@ -62,33 +61,33 @@ class IDPasswordTextField: UIView { self.addSubview(IDTextField) self.addSubview(passwordTextField) } - + func autoLayout() { IDLabel.snp.makeConstraints { label in label.top.equalTo(self.safeAreaLayoutGuide).offset(12) label.leading.equalTo(self.safeAreaLayoutGuide).offset(20) label.width.equalTo(47) } - + line.snp.makeConstraints { view in view.height.equalTo(1) view.top.equalTo(IDLabel.snp.bottom).offset(10.5) view.leading.trailing.equalTo(self.safeAreaLayoutGuide) } - + passwordLabel.snp.makeConstraints { label in label.top.equalTo(line.snp.bottom).offset(10.5) label.leading.equalTo(self.safeAreaLayoutGuide).offset(20) label.width.equalTo(62) } - + IDTextField.snp.makeConstraints { textField in textField.top.equalTo(self.safeAreaLayoutGuide).offset(11) textField.leading.equalTo(IDLabel.snp.trailing).offset(61) textField.trailing.equalTo(self.safeAreaLayoutGuide) textField.height.equalTo(22) } - + passwordTextField.snp.makeConstraints { textField in textField.top.equalTo(line.snp.bottom).offset(11) textField.leading.equalTo(passwordLabel.snp.trailing).offset(61) @@ -96,5 +95,4 @@ class IDPasswordTextField: UIView { textField.height.equalTo(22) } } - } diff --git a/iOS/issue-tracker/issue-tracker/View/Login/LoginView.swift b/iOS/issue-tracker/issue-tracker/View/Login/LoginView.swift index 821a3d6c6..913985c65 100644 --- a/iOS/issue-tracker/issue-tracker/View/Login/LoginView.swift +++ b/iOS/issue-tracker/issue-tracker/View/Login/LoginView.swift @@ -8,9 +8,8 @@ import UIKit import SnapKit -@IBDesignable class LoginView: UIView { - + let titleLabel: UILabel = { var label = UILabel() label.textAlignment = .center @@ -18,7 +17,7 @@ class LoginView: UIView { label.font = UIFont.systemFont(ofSize: 48) return label }() - + let login: UIButton = { var button = UIButton() button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16) @@ -26,7 +25,7 @@ class LoginView: UIView { button.setTitleColor(.systemBlue, for: .normal) return button }() - + let signUp: UIButton = { var button = UIButton() button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16) @@ -34,75 +33,71 @@ class LoginView: UIView { button.setTitleColor(.systemBlue, for: .normal) return button }() - - let appleLoginButton = GitHubLoginButton() - let githubLoginButton = AppleLoginButton() + +// let appleLoginButton = AppleLoginButton() +// let githubLoginButton = GitHubLoginButton() let textField = IDPasswordTextField() - + override init(frame: CGRect) { super.init(frame: frame) backgroundColor = #colorLiteral(red: 0.9490135312, green: 0.9490135312, blue: 0.9694761634, alpha: 1) addSubViews() setAutolayout() - } - + required init?(coder: NSCoder) { super.init(coder: coder) backgroundColor = #colorLiteral(red: 0.9490135312, green: 0.9490135312, blue: 0.9694761634, alpha: 1) addSubViews() setAutolayout() - } - + func addSubViews() { addSubview(titleLabel) - addSubview(appleLoginButton) - addSubview(githubLoginButton) +// addSubview(appleLoginButton) +// addSubview(githubLoginButton) addSubview(textField) addSubview(login) addSubview(signUp) } - + func setAutolayout() { titleLabel.snp.makeConstraints { title in title.height.equalTo(72) title.top.equalToSuperview().offset(165) title.leading.trailing.equalTo(self).inset(40) } - + textField.snp.makeConstraints { textField in textField.top.equalTo(titleLabel.snp.bottom).offset(72) textField.leading.trailing.equalTo(self) textField.height.equalTo(89) } - + login.snp.makeConstraints { login in login.top.equalTo(textField.snp.bottom).offset(32) login.leading.equalTo(self).inset(96) login.width.equalTo(45) login.height.equalTo(21) } - + signUp.snp.makeConstraints { signUp in signUp.top.equalTo(textField.snp.bottom).offset(32) signUp.leading.equalTo(login.snp.trailing).offset(79) signUp.width.equalTo(59) signUp.height.equalTo(21) } - - githubLoginButton.snp.makeConstraints { button in - button.top.equalTo(login.snp.bottom).offset(175) - button.leading.trailing.equalTo(self).inset(16) - button.height.equalTo(56) - } - - appleLoginButton.snp.makeConstraints { button in - button.top.equalTo(githubLoginButton.snp.bottom).offset(14) - button.leading.trailing.equalTo(self).inset(16) - button.height.equalTo(56) - } - + +// githubLoginButton.snp.makeConstraints { button in +// button.top.equalTo(login.snp.bottom).offset(175) +// button.leading.trailing.equalTo(self).inset(16) +// button.height.equalTo(56) +// } +// +// appleLoginButton.snp.makeConstraints { button in +// button.top.equalTo(githubLoginButton.snp.bottom).offset(14) +// button.leading.trailing.equalTo(self).inset(16) +// button.height.equalTo(56) +// } } - } diff --git a/iOS/issue-tracker/issue-tracker/View/Milestone/AddMilestoneTableViewCell.swift b/iOS/issue-tracker/issue-tracker/View/Milestone/AddMilestoneTableViewCell.swift new file mode 100644 index 000000000..b8c84b5e7 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/View/Milestone/AddMilestoneTableViewCell.swift @@ -0,0 +1,29 @@ +// +// AddTableViewCell.swift +// issue-tracker +// +// Created by Ador on 2021/06/16. +// + +import UIKit + +class AddMilestoneTableViewCell: UITableViewCell { + + static let reuseIdentifier = "AddMilestoneTableViewCell" + var textField = UITextField() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + textField.frame = CGRect(x: self.frame.origin.x + 100, y: self.frame.origin.y, width: self.frame.width - 100, height: 44) + contentView.addSubview(textField) + } + required init?(coder: NSCoder) { + super.init(coder: coder) + textField.frame = CGRect(x: self.frame.origin.x + 100, y: self.frame.origin.y, width: self.frame.width - 100, height: 44) + contentView.addSubview(textField) + } + + func bind(_ completion: (UITextField) -> Void) { + completion(textField) + } +} diff --git a/iOS/issue-tracker/issue-tracker/View/Milestone/MilestoneTableViewCell.swift b/iOS/issue-tracker/issue-tracker/View/Milestone/MilestoneTableViewCell.swift index 44e85906a..68cf3a99c 100644 --- a/iOS/issue-tracker/issue-tracker/View/Milestone/MilestoneTableViewCell.swift +++ b/iOS/issue-tracker/issue-tracker/View/Milestone/MilestoneTableViewCell.swift @@ -11,25 +11,17 @@ import SnapKit class MilestoneTableViewCell: UITableViewCell { static let reuseId = "MilestoneTableViewCell" - + private let verticalStackView: UIStackView = { let stackView = UIStackView() - stackView.spacing = 10 + stackView.spacing = 5 stackView.axis = .vertical stackView.alignment = .fill stackView.distribution = .fillEqually return stackView }() - - private let horizenStackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.alignment = .top - stackView.distribution = .fill - return stackView - }() - - private let IssueLabelStackView: UIStackView = { + + private let issueLabelStackView: UIStackView = { let stackView = UIStackView() stackView.spacing = 5 stackView.axis = .horizontal @@ -37,35 +29,25 @@ class MilestoneTableViewCell: UITableViewCell { stackView.distribution = .fillEqually return stackView }() - + private let titleLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 22, weight: .bold) - label.text = "마일스톤 타이틀" label.textAlignment = .left return label }() - - private let achievementLabel: UILabel = { - let label = UILabel() - label.text = "50%" - label.textAlignment = .right - return label - }() - + private let descriptionLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 17) label.textColor = .systemGray - label.text = "마일스톤 설명" return label }() - + private let dueDateLabel: UILabel = { let label = UILabel() - label.font = .systemFont(ofSize: 17) + label.font = .systemFont(ofSize: 14) label.textColor = .systemGray - label.text = "2021-06-21" return label }() @@ -73,58 +55,69 @@ class MilestoneTableViewCell: UITableViewCell { let label = PaddingLabel(withInsets: 5, 5, 10, 10) label.textAlignment = .center label.textColor = .white + label.font = .systemFont(ofSize: 14) label.layer.masksToBounds = true - label.layer.cornerRadius = 15 - label.text = "Opend Issue 1개" - label.backgroundColor = UIColor.hexStringToUIColor(hex: "#B1CAE5") + label.layer.cornerRadius = 8 + label.backgroundColor = .systemGreen return label }() - + private let closedIssue: PaddingLabel = { - let label = PaddingLabel(withInsets: 5, 5, 10, 10) + let label = PaddingLabel(withInsets: 10, 10, 10, 10) label.textAlignment = .center label.textColor = .white + label.font = .systemFont(ofSize: 14) label.layer.masksToBounds = true - label.layer.cornerRadius = 15 - label.text = "Closed Issue 1개" - label.backgroundColor = UIColor.hexStringToUIColor(hex: "#DFCD85") + label.layer.cornerRadius = 8 + label.backgroundColor = .systemRed return label }() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - IssueLabelStackView.addArrangedSubview(openedIssue) - IssueLabelStackView.addArrangedSubview(closedIssue) - + issueLabelStackView.addArrangedSubview(openedIssue) + issueLabelStackView.addArrangedSubview(closedIssue) + verticalStackView.addArrangedSubview(titleLabel) verticalStackView.addArrangedSubview(descriptionLabel) verticalStackView.addArrangedSubview(dueDateLabel) - verticalStackView.addArrangedSubview(IssueLabelStackView) - - horizenStackView.addArrangedSubview(verticalStackView) - horizenStackView.addArrangedSubview(achievementLabel) - addSubview(horizenStackView) + verticalStackView.addArrangedSubview(issueLabelStackView) + + contentView.addSubview(verticalStackView) } - + required init?(coder: NSCoder) { super.init(coder: coder) - IssueLabelStackView.addArrangedSubview(openedIssue) - IssueLabelStackView.addArrangedSubview(closedIssue) - + issueLabelStackView.addArrangedSubview(openedIssue) + issueLabelStackView.addArrangedSubview(closedIssue) + verticalStackView.addArrangedSubview(titleLabel) verticalStackView.addArrangedSubview(descriptionLabel) verticalStackView.addArrangedSubview(dueDateLabel) - verticalStackView.addArrangedSubview(IssueLabelStackView) - - horizenStackView.addArrangedSubview(verticalStackView) - horizenStackView.addArrangedSubview(achievementLabel) - addSubview(horizenStackView) + verticalStackView.addArrangedSubview(issueLabelStackView) + + contentView.addSubview(verticalStackView) } override func layoutSubviews() { super.layoutSubviews() - horizenStackView.snp.makeConstraints { (maker) in + verticalStackView.snp.makeConstraints { (maker) in maker.edges.equalToSuperview().inset(20) } + contentView.frame = contentView.frame.inset(by: UIEdgeInsets(top: 10, left: 2, bottom: 0, right: 2)) + contentView.layer.cornerRadius = 20 + contentView.layer.borderWidth = 2 + } + + func configure(with milestone: Milestone) { + titleLabel.text = milestone.title + descriptionLabel.text = milestone.description + dueDateLabel.text = milestone.dueDate + if let open = milestone.openedIssueCount { + openedIssue.text = "열린 이슈 \(open)개" + } + if let close = milestone.closedIssueCount { + closedIssue.text = "닫힌 이슈 \(close)개" + } } } diff --git a/iOS/issue-tracker/issue-tracker/ViewModel/AddLabelViewModel.swift b/iOS/issue-tracker/issue-tracker/ViewModel/AddLabelViewModel.swift new file mode 100644 index 000000000..b74b77d6a --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/ViewModel/AddLabelViewModel.swift @@ -0,0 +1,35 @@ +// +// AddLabelViewModel.swift +// issue-tracker +// +// Created by zeke on 2021/06/16. +// + +import Foundation +import RxCocoa + +class AddLabelViewModel { + var title = BehaviorRelay(value: "") + var description = BehaviorRelay(value: "") + var color = BehaviorRelay(value: "3DDCFF") + var fontColor = "#FFFFFF" + var networkManager: Networkable + + init(networkManager: Networkable) { + self.networkManager = networkManager + } + + func postAddedLabel(completion: @escaping () -> Void) { + let encodableLabel = IssueLabel(id: nil, title: title.value, color: color.value, fontColor: nil, description: description.value) + networkManager.postRequest(url: Endpoint(path: .label).url()!, encodable: encodableLabel, completion: { result in + switch result { + case .success: + CustomAlertView.shared.setUpAlertView(title: "성공", message: "라벨이 등록되었습니다.", buttonTitle: "확인", alertType: .success, buttonHandler: { + completion() + }) + case .failure: + CustomAlertView.shared.setUpAlertView(title: "실패", message: "서버가 불안정합니다. 다시 시도해주세요.", buttonTitle: "확인", alertType: .failure, buttonHandler: nil) + } + }) + } +} diff --git a/iOS/issue-tracker/issue-tracker/ViewModel/IssueDetailViewModel.swift b/iOS/issue-tracker/issue-tracker/ViewModel/IssueDetailViewModel.swift new file mode 100644 index 000000000..7abaffb58 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/ViewModel/IssueDetailViewModel.swift @@ -0,0 +1,33 @@ +// +// IssueDetailViewModel.swift +// issue-tracker +// +// Created by Ador on 2021/06/22. +// + +import Foundation +import RxCocoa + +class IssueDetailViewModel { + var subject: BehaviorRelay = BehaviorRelay(value: nil) + + func fetch(id: Int) { + guard let url = Endpoint(path: .issue).url(id: id) else { return } + NetworkManager().request(url: url, decodableType: IssueDetail.self) { [weak self] data in + self?.subject.accept(data) + } + } + + func post(comment: String) { + guard let url = Endpoint(path: .issue).url(id: 1) else { return } + let post = PostComment(writer: "Soo", comment: comment) + NetworkManager().postRequest(url: url, encodable: post) { result in + switch result { + case .success: + CustomAlertView.shared.setUpAlertView(title: "성공", message: "라벨이 등록되었습니다.", buttonTitle: "확인", alertType: .success, buttonHandler: nil) + case .failure: + CustomAlertView.shared.setUpAlertView(title: "실패", message: "서버가 불안정합니다. 다시 시도해주세요.", buttonTitle: "확인", alertType: .failure, buttonHandler: nil) + } + } + } +} diff --git a/iOS/issue-tracker/issue-tracker/ViewModel/IssueListViewModel.swift b/iOS/issue-tracker/issue-tracker/ViewModel/IssueListViewModel.swift new file mode 100644 index 000000000..516cadba8 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/ViewModel/IssueListViewModel.swift @@ -0,0 +1,57 @@ +// +// IssueListViewModel.swift +// issue-tracker +// +// Created by 양준혁 on 2021/06/21. +// + +import Foundation +import RxCocoa +import RxSwift + +class IssueListViewModel { + + let issueList = BehaviorRelay<[Issue]>(value: []) + let networkManager: Networkable + var selectMode = BehaviorRelay(value: false) + var selectedCell = BehaviorRelay<[Issue]>(value: []) + + init(networkManager: Networkable) { + self.networkManager = networkManager + } + + func fetchIssueList(filterBy string: String? = nil) { + let query = URLQueryItem(name: "is", value: string) + networkManager.request(url: Endpoint(path: .issue).url(queryItems: [query])!, decodableType: IssueList.self) { issueList in + self.issueList.accept(issueList.data) + } + } + + func deleteIssue(id: Int) { + networkManager.deleteRequest(url: Endpoint(path: .issue).url(id: id)!) { result in + switch result { + case .success: + CustomAlertView.shared.setUpAlertView(title: "성공", message: "이슈가 성공적으로 삭제되었습니다.", buttonTitle: "확인", alertType: .success, buttonHandler: { + self.fetchIssueList() + }) + case .failure: + CustomAlertView.shared.setUpAlertView(title: "실패", message: "서버가 불안정합니다. 다시 시도해주세요.", buttonTitle: "확인", alertType: .failure, buttonHandler: nil) + } + } + } + + func patchIssue(issues: [Issue]) { + let encodableObject = PatchIssue(issueNumber: issues.map { $0.id! }, isOpen: false) + networkManager.patchRequest(url: Endpoint(path: .issue).url()!, encodable: encodableObject) { result in + switch result { + case .success: + CustomAlertView.shared.setUpAlertView(title: "성공", message: "이슈가 성공적으로 닫혔습니다.", buttonTitle: "확인", alertType: .success, buttonHandler: { + self.fetchIssueList() + }) + case .failure(let error): + print(error) + CustomAlertView.shared.setUpAlertView(title: "실패", message: "서버가 불안정합니다. 다시 시도해주세요.", buttonTitle: "확인", alertType: .failure, buttonHandler: nil) + } + } + } +} diff --git a/iOS/issue-tracker/issue-tracker/ViewModel/LabelListViewModel.swift b/iOS/issue-tracker/issue-tracker/ViewModel/LabelListViewModel.swift new file mode 100644 index 000000000..e581f082c --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/ViewModel/LabelListViewModel.swift @@ -0,0 +1,39 @@ +// +// LabelViewModel.swift +// issue-tracker +// +// Created by 양준혁 on 2021/06/15. +// + +import Foundation +import RxCocoa + +class LabelListViewModel { + let networkManager: NetworkManager + var labelList = BehaviorRelay<[IssueLabel]>(value: []) + + init(networkManager: NetworkManager) { + self.networkManager = networkManager + fetchLabelList() + } + + func fetchLabelList() { + networkManager.request(url: Endpoint(path: .label).url()!, decodableType: LabelList.self) { [weak self] label in + self?.labelList.accept(label.data) + } + } + + func deleteLabel(id: Int) { + networkManager.deleteRequest(url: Endpoint(path: .label).url(id: id)!) { result in + switch result { + case .success: + CustomAlertView.shared.setUpAlertView(title: "성공", message: "라벨이 성공적으로 삭제되었습니다.", buttonTitle: "확인", alertType: .success, buttonHandler: { + self.fetchLabelList() + }) + case .failure(let error): + print(error) + CustomAlertView.shared.setUpAlertView(title: "실패", message: "서버가 불안정합니다. 다시 시도해주세요.", buttonTitle: "확인", alertType: .failure, buttonHandler: nil) + } + } + } +} diff --git a/iOS/issue-tracker/issue-tracker/ViewModel/MilestoneViewModel.swift b/iOS/issue-tracker/issue-tracker/ViewModel/MilestoneViewModel.swift new file mode 100644 index 000000000..2239ef3b7 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/ViewModel/MilestoneViewModel.swift @@ -0,0 +1,75 @@ +// +// MilestoneViewModel.swift +// issue-tracker +// +// Created by Ador on 2021/06/16. +// + +import Foundation +import RxCocoa + +class MilestoneViewModel { + static let shared = MilestoneViewModel() + private let networkManager = NetworkManager() + var subject: BehaviorRelay<[Milestone]> = BehaviorRelay<[Milestone]>(value: []) + var milestone: [String: String] = [:] + var dismissCompletion: (() -> Void)? + private var url: URL! { + return Endpoint(path: .milestone).url()! + } + + init() { + fetch() + } + + private func fetch(completion: (() -> Void)? = nil) { + networkManager.request(url: url, decodableType: MilestoneList.self) { [weak self] data in + self?.subject.accept(data.data) + completion?() + } + } + + func post() { + guard checkInput() else { + CustomAlertView.shared.setUpAlertView(title: "실패", message: "제목을 반드시 입력해주세요!", buttonTitle: "확인", alertType: .failure, buttonHandler: nil) + return + } + guard let title = milestone["title"] else { + fatalError() + } + let description = milestone["description"] + let dueDate = milestone["dueDate"] + let mile = Milestone(id: 1, title: title, description: description, createdTime: nil, dueDate: dueDate, closedIssueCount: nil, openedIssueCount: nil) + networkManager.postRequest(url: url, encodable: mile) { result in + switch result { + case .success: + CustomAlertView.shared.setUpAlertView(title: "성공", + message: "마일스톤이 등록되었습니다.", + buttonTitle: "확인", + alertType: .success, + buttonHandler: { [weak self] in + self?.fetch() + self?.dismissCompletion?() + }) + case .failure: + CustomAlertView.shared.setUpAlertView(title: "실패", message: "서버가 불안정합니다. 다시 시도해주세요.", buttonTitle: "확인", alertType: .failure, buttonHandler: nil) + } + } + } + + func delete(id: Int) { + guard let url = Endpoint(path: .milestone).url(id: id) else { return } + networkManager.deleteRequest(url: url) { [weak self] _ in + self?.fetch() + self?.dismissCompletion?() + } + } + + private func checkInput() -> Bool { + // 타이틀 입력 체크 + guard let title = milestone["title"], !title.isEmpty, title != "" else { + return false + } + return true + } +} diff --git a/iOS/issue-tracker/issue-tracker/ViewModel/NewIssueViewModel.swift b/iOS/issue-tracker/issue-tracker/ViewModel/NewIssueViewModel.swift new file mode 100644 index 000000000..249ab19c9 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/ViewModel/NewIssueViewModel.swift @@ -0,0 +1,32 @@ +// +// NewIssueViewModel.swift +// issue-tracker +// +// Created by Ador on 2021/06/21. +// + +import Foundation + +class NewIssueViewModel { + var title: String? + var content: String? + + func post(completion: @escaping () -> Void) { + guard let url = Endpoint(path: .issue).url() else { + return + } + guard let title = title, let content = content, !content.isEmpty else { + assertionFailure("issue post fail") + return + } + let newIssue = NewIssue(title: title, comment: content) + NetworkManager().postRequest(url: url, encodable: newIssue) { result in + switch result { + case .success: + CustomAlertView.shared.setUpAlertView(title: "성공", message: "라벨이 등록되었습니다.", buttonTitle: "확인", alertType: .success, buttonHandler: completion) + case .failure: + CustomAlertView.shared.setUpAlertView(title: "실패", message: "서버가 불안정합니다. 다시 시도해주세요.", buttonTitle: "확인", alertType: .failure, buttonHandler: nil) + } + } + } +}