diff --git a/iOS/issue-tracker/issue-tracker.xcodeproj/project.pbxproj b/iOS/issue-tracker/issue-tracker.xcodeproj/project.pbxproj index 7b6a125e6..d0ef93a9f 100644 --- a/iOS/issue-tracker/issue-tracker.xcodeproj/project.pbxproj +++ b/iOS/issue-tracker/issue-tracker.xcodeproj/project.pbxproj @@ -14,7 +14,6 @@ E426DAC42671D95D0069E77D /* LoginService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E426DAC32671D95D0069E77D /* LoginService.swift */; }; E426DAC72671F6A00069E77D /* LoginError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E426DAC62671F6A00069E77D /* LoginError.swift */; }; E426DAC92671F7760069E77D /* AlertFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E426DAC82671F7760069E77D /* AlertFactory.swift */; }; - E426DACB267206D10069E77D /* LoginInfoContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E426DACA267206D10069E77D /* LoginInfoContainer.swift */; }; E426DAD126730DC50069E77D /* OAuthResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = E426DAD026730DC50069E77D /* OAuthResponseDTO.swift */; }; E42AD0FE266E52FB0071B436 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42AD0FD266E52FB0071B436 /* AppDelegate.swift */; }; E42AD100266E52FB0071B436 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42AD0FF266E52FB0071B436 /* SceneDelegate.swift */; }; @@ -25,18 +24,66 @@ E42AD1182670886F0071B436 /* IssueTrackerTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42AD1172670886F0071B436 /* IssueTrackerTabBarController.swift */; }; E42AD11E267093320071B436 /* IssueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42AD11D267093320071B436 /* IssueViewController.swift */; }; E42AD120267093430071B436 /* LabelViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42AD11F267093430071B436 /* LabelViewController.swift */; }; - E42AD122267093590071B436 /* MilestoneViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42AD121267093590071B436 /* MilestoneViewController.swift */; }; + E42AD122267093590071B436 /* MileStoneViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42AD121267093590071B436 /* MileStoneViewController.swift */; }; E42AD124267093700071B436 /* MyAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42AD123267093700071B436 /* MyAccountViewController.swift */; }; E42AD12626709C010071B436 /* UIViewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42AD12526709C010071B436 /* UIViewControllerExtension.swift */; }; + E43912C726776D8F003CD344 /* LabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43912C626776D8F003CD344 /* LabelView.swift */; }; + E43912CE26777310003CD344 /* HexColorConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43912CD26777310003CD344 /* HexColorConverter.swift */; }; + E43912D0267878BF003CD344 /* HexColorConvertable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43912CF267878BF003CD344 /* HexColorConvertable.swift */; }; + E43912D2267878F4003CD344 /* HexColorCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43912D1267878F4003CD344 /* HexColorCode.swift */; }; + E43912D426788CC3003CD344 /* ImageBarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43912D326788CC3003CD344 /* ImageBarButton.swift */; }; + E43912D626789812003CD344 /* RequestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43912D526789812003CD344 /* RequestManager.swift */; }; + E43912D826789A83003CD344 /* EndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43912D726789A83003CD344 /* EndPoint.swift */; }; + E43912DA26789E16003CD344 /* Label.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43912D926789E16003CD344 /* Label.swift */; }; + E43912DC2678A737003CD344 /* JWT.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43912DB2678A737003CD344 /* JWT.swift */; }; + E43912DE2678A76D003CD344 /* RequestKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43912DD2678A76D003CD344 /* RequestKeys.swift */; }; + E43912E02678AD0A003CD344 /* LabelTableViewDatasource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43912DF2678AD0A003CD344 /* LabelTableViewDatasource.swift */; }; + E43912E8267A1FE5003CD344 /* NewLabelDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43912E7267A1FE5003CD344 /* NewLabelDTO.swift */; }; + E43912EA267A2351003CD344 /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43912E9267A2351003CD344 /* NetworkError.swift */; }; + E43912EC267B1A4E003CD344 /* LoginInfoDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43912EB267B1A4E003CD344 /* LoginInfoDTO.swift */; }; + E43912EE267C3C0E003CD344 /* CellAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43912ED267C3C0E003CD344 /* CellAction.swift */; }; + E43912F0267C3EED003CD344 /* LabelControlViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43912EF267C3EED003CD344 /* LabelControlViewController.swift */; }; + E43912F4267CAA8D003CD344 /* UITextFieldExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43912F3267CAA8D003CD344 /* UITextFieldExtension.swift */; }; + E43912F6267CAE01003CD344 /* TopMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43912F5267CAE01003CD344 /* TopMenuView.swift */; }; + E43912F8267CB3D7003CD344 /* MultipleLineInputStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43912F7267CB3D7003CD344 /* MultipleLineInputStackView.swift */; }; + E46E11242677391B005375A1 /* LabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46E11232677391B005375A1 /* LabelTableViewCell.swift */; }; + E49695672680B0B7001AEB89 /* IssueEditViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49695662680B0B7001AEB89 /* IssueEditViewController.swift */; }; + E496956E2681905A001AEB89 /* IssueInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E496956D2681905A001AEB89 /* IssueInfoView.swift */; }; + E49695762681E7B7001AEB89 /* Down in Frameworks */ = {isa = PBXBuildFile; productRef = E49695752681E7B7001AEB89 /* Down */; }; + E49695782682C4B0001AEB89 /* AdditionalInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49695772682C4B0001AEB89 /* AdditionalInfoViewController.swift */; }; + E496957A2682CB33001AEB89 /* UITableCellExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49695792682CB33001AEB89 /* UITableCellExtension.swift */; }; + E496957C2682D2CE001AEB89 /* CommonDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = E496957B2682D2CE001AEB89 /* CommonDTO.swift */; }; + E496957E2682D8D1001AEB89 /* LabelInfoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E496957D2682D8D1001AEB89 /* LabelInfoTableViewCell.swift */; }; + E49695822682DCEC001AEB89 /* AdditionalInfoTableDatasource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49695812682DCEC001AEB89 /* AdditionalInfoTableDatasource.swift */; }; + E49695862682FD95001AEB89 /* AdditionalInfoTableDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49695852682FD95001AEB89 /* AdditionalInfoTableDelegate.swift */; }; + E49695882682FDC9001AEB89 /* CellSelectionTableDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49695872682FDC9001AEB89 /* CellSelectionTableDelegate.swift */; }; + E496958C26830DCE001AEB89 /* MilestoneInfoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E496958B26830DCE001AEB89 /* MilestoneInfoTableViewCell.swift */; }; + E4969590268317C1001AEB89 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = E496958F268317C1001AEB89 /* User.swift */; }; + E4969592268318A4001AEB89 /* AssigneeInfoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4969591268318A4001AEB89 /* AssigneeInfoTableViewCell.swift */; }; + E496959726832693001AEB89 /* IssueTableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E496959626832693001AEB89 /* IssueTableViewDelegate.swift */; }; + E496959926832B5A001AEB89 /* NewIssue.swift in Sources */ = {isa = PBXBuildFile; fileRef = E496959826832B5A001AEB89 /* NewIssue.swift */; }; + E496959B268334BC001AEB89 /* CommonInfoTableDatasource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E496959A268334BC001AEB89 /* CommonInfoTableDatasource.swift */; }; E4F7E8E826732B6300CE51C2 /* TabBarChildInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F7E8E726732B6300CE51C2 /* TabBarChildInfo.swift */; }; E4F7E8EA2673394300CE51C2 /* ImageLoadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F7E8E92673394300CE51C2 /* ImageLoadManager.swift */; }; E4F7E8EC26733A9700CE51C2 /* IssueTrackerTabBarCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F7E8EB26733A9700CE51C2 /* IssueTrackerTabBarCreator.swift */; }; + FA431FF9267F36FA0010EA91 /* MileStoneControlViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA431FF8267F36FA0010EA91 /* MileStoneControlViewController.swift */; }; + FA431FFB267F3B680010EA91 /* NewMileStoneDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA431FFA267F3B680010EA91 /* NewMileStoneDTO.swift */; }; + FA431FFD267F61150010EA91 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA431FFC267F61150010EA91 /* String.swift */; }; + FA733A2E267C3D77005A40C8 /* Milestone.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA733A2D267C3D77005A40C8 /* Milestone.swift */; }; + FA733A30267C3E9E005A40C8 /* MilestoneTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA733A2F267C3E9E005A40C8 /* MilestoneTableViewDataSource.swift */; }; + FAB2C36A26789D41009F879C /* MileStoneTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB2C36926789D40009F879C /* MileStoneTableViewCell.swift */; }; + FAB2C36C2679F6CC009F879C /* MileStoneLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB2C36B2679F6CC009F879C /* MileStoneLabelView.swift */; }; FAC6D88B2671D5A100A9E5F9 /* GithubAuthorizationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6D88A2671D5A100A9E5F9 /* GithubAuthorizationManager.swift */; }; FAC6D88D2671D8A100A9E5F9 /* SocialLoginManagable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6D88C2671D8A100A9E5F9 /* SocialLoginManagable.swift */; }; FAEB30AC266F5E9700C17BE9 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEB30AB266F5E9700C17BE9 /* Colors.swift */; }; FAEB30AE266F6A7800C17BE9 /* UIImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEB30AD266F6A7800C17BE9 /* UIImageExtension.swift */; }; FAEB30B12670B74000C17BE9 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = FAEB30B02670B74000C17BE9 /* Alamofire */; }; FAEB30B42670B76200C17BE9 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEB30B32670B76200C17BE9 /* NetworkManager.swift */; }; + FAF927AB2680DB1C0017F8DE /* IssueTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF927AA2680DB1C0017F8DE /* IssueTableViewDataSource.swift */; }; + FAF927AD2680DCCA0017F8DE /* IssueTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF927AC2680DCCA0017F8DE /* IssueTableViewCell.swift */; }; + FAF927AF2681DA810017F8DE /* Issue.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF927AE2681DA810017F8DE /* Issue.swift */; }; + FAF927B126831A570017F8DE /* IssueDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF927B026831A570017F8DE /* IssueDetailViewController.swift */; }; + FAFD11B1268078F80064AED4 /* CommonTableDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFD11B0268078F80064AED4 /* CommonTableDelegate.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -47,7 +94,6 @@ E426DAC32671D95D0069E77D /* LoginService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginService.swift; sourceTree = ""; }; E426DAC62671F6A00069E77D /* LoginError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginError.swift; sourceTree = ""; }; E426DAC82671F7760069E77D /* AlertFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertFactory.swift; sourceTree = ""; }; - E426DACA267206D10069E77D /* LoginInfoContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginInfoContainer.swift; sourceTree = ""; }; E426DAD026730DC50069E77D /* OAuthResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthResponseDTO.swift; sourceTree = ""; }; E42AD0FA266E52FB0071B436 /* issue-tracker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "issue-tracker.app"; sourceTree = BUILT_PRODUCTS_DIR; }; E42AD0FD266E52FB0071B436 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -58,19 +104,66 @@ E42AD109266E52FE0071B436 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; E42AD10B266E52FE0071B436 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E42AD1172670886F0071B436 /* IssueTrackerTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueTrackerTabBarController.swift; sourceTree = ""; }; - E42AD11D267093320071B436 /* IssueViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueViewController.swift; sourceTree = ""; }; + E42AD11D267093320071B436 /* IssueViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = IssueViewController.swift; path = IssueEditing/IssueViewController.swift; sourceTree = ""; }; E42AD11F267093430071B436 /* LabelViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelViewController.swift; sourceTree = ""; }; - E42AD121267093590071B436 /* MilestoneViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MilestoneViewController.swift; sourceTree = ""; }; + E42AD121267093590071B436 /* MileStoneViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MileStoneViewController.swift; sourceTree = ""; }; E42AD123267093700071B436 /* MyAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAccountViewController.swift; sourceTree = ""; }; E42AD12526709C010071B436 /* UIViewControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerExtension.swift; sourceTree = ""; }; + E43912C626776D8F003CD344 /* LabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelView.swift; sourceTree = ""; }; + E43912CD26777310003CD344 /* HexColorConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HexColorConverter.swift; sourceTree = ""; }; + E43912CF267878BF003CD344 /* HexColorConvertable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HexColorConvertable.swift; sourceTree = ""; }; + E43912D1267878F4003CD344 /* HexColorCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HexColorCode.swift; sourceTree = ""; }; + E43912D326788CC3003CD344 /* ImageBarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBarButton.swift; sourceTree = ""; }; + E43912D526789812003CD344 /* RequestManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestManager.swift; sourceTree = ""; }; + E43912D726789A83003CD344 /* EndPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndPoint.swift; sourceTree = ""; }; + E43912D926789E16003CD344 /* Label.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Label.swift; sourceTree = ""; }; + E43912DB2678A737003CD344 /* JWT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JWT.swift; sourceTree = ""; }; + E43912DD2678A76D003CD344 /* RequestKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestKeys.swift; sourceTree = ""; }; + E43912DF2678AD0A003CD344 /* LabelTableViewDatasource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelTableViewDatasource.swift; sourceTree = ""; }; + E43912E7267A1FE5003CD344 /* NewLabelDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewLabelDTO.swift; sourceTree = ""; }; + E43912E9267A2351003CD344 /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; + E43912EB267B1A4E003CD344 /* LoginInfoDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginInfoDTO.swift; sourceTree = ""; }; + E43912ED267C3C0E003CD344 /* CellAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellAction.swift; sourceTree = ""; }; + E43912EF267C3EED003CD344 /* LabelControlViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelControlViewController.swift; sourceTree = ""; }; + E43912F3267CAA8D003CD344 /* UITextFieldExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITextFieldExtension.swift; sourceTree = ""; }; + E43912F5267CAE01003CD344 /* TopMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopMenuView.swift; sourceTree = ""; }; + E43912F7267CB3D7003CD344 /* MultipleLineInputStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleLineInputStackView.swift; sourceTree = ""; }; + E46E11232677391B005375A1 /* LabelTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelTableViewCell.swift; sourceTree = ""; }; + E49695662680B0B7001AEB89 /* IssueEditViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueEditViewController.swift; sourceTree = ""; }; + E496956D2681905A001AEB89 /* IssueInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueInfoView.swift; sourceTree = ""; }; + E49695772682C4B0001AEB89 /* AdditionalInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdditionalInfoViewController.swift; sourceTree = ""; }; + E49695792682CB33001AEB89 /* UITableCellExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITableCellExtension.swift; sourceTree = ""; }; + E496957B2682D2CE001AEB89 /* CommonDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonDTO.swift; sourceTree = ""; }; + E496957D2682D8D1001AEB89 /* LabelInfoTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelInfoTableViewCell.swift; sourceTree = ""; }; + E49695812682DCEC001AEB89 /* AdditionalInfoTableDatasource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdditionalInfoTableDatasource.swift; sourceTree = ""; }; + E49695852682FD95001AEB89 /* AdditionalInfoTableDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdditionalInfoTableDelegate.swift; sourceTree = ""; }; + E49695872682FDC9001AEB89 /* CellSelectionTableDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellSelectionTableDelegate.swift; sourceTree = ""; }; + E496958B26830DCE001AEB89 /* MilestoneInfoTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MilestoneInfoTableViewCell.swift; sourceTree = ""; }; + E496958F268317C1001AEB89 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + E4969591268318A4001AEB89 /* AssigneeInfoTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssigneeInfoTableViewCell.swift; sourceTree = ""; }; + E496959626832693001AEB89 /* IssueTableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueTableViewDelegate.swift; sourceTree = ""; }; + E496959826832B5A001AEB89 /* NewIssue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewIssue.swift; sourceTree = ""; }; + E496959A268334BC001AEB89 /* CommonInfoTableDatasource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonInfoTableDatasource.swift; sourceTree = ""; }; E4F7E8E726732B6300CE51C2 /* TabBarChildInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarChildInfo.swift; sourceTree = ""; }; E4F7E8E92673394300CE51C2 /* ImageLoadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLoadManager.swift; sourceTree = ""; }; E4F7E8EB26733A9700CE51C2 /* IssueTrackerTabBarCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueTrackerTabBarCreator.swift; sourceTree = ""; }; + FA431FF8267F36FA0010EA91 /* MileStoneControlViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MileStoneControlViewController.swift; sourceTree = ""; }; + FA431FFA267F3B680010EA91 /* NewMileStoneDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewMileStoneDTO.swift; sourceTree = ""; }; + FA431FFC267F61150010EA91 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; + FA733A2D267C3D77005A40C8 /* Milestone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Milestone.swift; sourceTree = ""; }; + FA733A2F267C3E9E005A40C8 /* MilestoneTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MilestoneTableViewDataSource.swift; sourceTree = ""; }; + FAB2C36926789D40009F879C /* MileStoneTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MileStoneTableViewCell.swift; sourceTree = ""; }; + FAB2C36B2679F6CC009F879C /* MileStoneLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MileStoneLabelView.swift; sourceTree = ""; }; FAC6D88A2671D5A100A9E5F9 /* GithubAuthorizationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubAuthorizationManager.swift; sourceTree = ""; }; FAC6D88C2671D8A100A9E5F9 /* SocialLoginManagable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialLoginManagable.swift; sourceTree = ""; }; FAEB30AB266F5E9700C17BE9 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; }; FAEB30AD266F6A7800C17BE9 /* UIImageExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageExtension.swift; sourceTree = ""; }; FAEB30B32670B76200C17BE9 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; + FAF927AA2680DB1C0017F8DE /* IssueTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueTableViewDataSource.swift; sourceTree = ""; }; + FAF927AC2680DCCA0017F8DE /* IssueTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueTableViewCell.swift; sourceTree = ""; }; + FAF927AE2681DA810017F8DE /* Issue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Issue.swift; sourceTree = ""; }; + FAF927B026831A570017F8DE /* IssueDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueDetailViewController.swift; sourceTree = ""; }; + FAFD11B0268078F80064AED4 /* CommonTableDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonTableDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -79,6 +172,7 @@ buildActionMask = 2147483647; files = ( FAEB30B12670B74000C17BE9 /* Alamofire in Frameworks */, + E49695762681E7B7001AEB89 /* Down in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -88,8 +182,9 @@ E426DACC267301010069E77D /* Model */ = { isa = PBXGroup; children = ( - E426DAC32671D95D0069E77D /* LoginService.swift */, E426DABB2670EC5E0069E77D /* LoginInfo.swift */, + E43912EB267B1A4E003CD344 /* LoginInfoDTO.swift */, + E426DAC32671D95D0069E77D /* LoginService.swift */, E426DAC62671F6A00069E77D /* LoginError.swift */, E426DACD267302BA0069E77D /* Protocol */, FAC6D8922671E1A800A9E5F9 /* Github */, @@ -156,6 +251,9 @@ FAEB30AD266F6A7800C17BE9 /* UIImageExtension.swift */, E42AD12526709C010071B436 /* UIViewControllerExtension.swift */, E426DAC82671F7760069E77D /* AlertFactory.swift */, + E43912F3267CAA8D003CD344 /* UITextFieldExtension.swift */, + FA431FFC267F61150010EA91 /* String.swift */, + E49695792682CB33001AEB89 /* UITableCellExtension.swift */, ); path = Common; sourceTree = ""; @@ -181,11 +279,11 @@ isa = PBXGroup; children = ( E42AD116267087F40071B436 /* TabBar */, - E426DACA267206D10069E77D /* LoginInfoContainer.swift */, E42AD11926708EBA0071B436 /* Issue */, E42AD11A26708EC00071B436 /* Label */, E42AD11B26708EC80071B436 /* Milestone */, E42AD11C26708ED50071B436 /* MyAccount */, + E43912C826776D95003CD344 /* Common */, ); path = Main; sourceTree = ""; @@ -203,7 +301,9 @@ E42AD11926708EBA0071B436 /* Issue */ = { isa = PBXGroup; children = ( - E42AD11D267093320071B436 /* IssueViewController.swift */, + FAF927A52680D9560017F8DE /* Model */, + FAF927A62680D95B0017F8DE /* View */, + FAF927A72680D9630017F8DE /* Controller */, ); path = Issue; sourceTree = ""; @@ -211,7 +311,9 @@ E42AD11A26708EC00071B436 /* Label */ = { isa = PBXGroup; children = ( - E42AD11F267093430071B436 /* LabelViewController.swift */, + E43912CC267772E4003CD344 /* Model */, + E43912CA267772D3003CD344 /* View */, + E43912CB267772DA003CD344 /* Controller */, ); path = Label; sourceTree = ""; @@ -219,7 +321,9 @@ E42AD11B26708EC80071B436 /* Milestone */ = { isa = PBXGroup; children = ( - E42AD121267093590071B436 /* MilestoneViewController.swift */, + FAB2C36626789ADD009F879C /* Model */, + FAB2C36726789AE3009F879C /* View */, + FAB2C36826789AE6009F879C /* Controller */, ); path = Milestone; sourceTree = ""; @@ -232,6 +336,90 @@ path = MyAccount; sourceTree = ""; }; + E43912C826776D95003CD344 /* Common */ = { + isa = PBXGroup; + children = ( + E43912C926776DA0003CD344 /* View */, + E43912ED267C3C0E003CD344 /* CellAction.swift */, + FAFD11B0268078F80064AED4 /* CommonTableDelegate.swift */, + E496957B2682D2CE001AEB89 /* CommonDTO.swift */, + ); + path = Common; + sourceTree = ""; + }; + E43912C926776DA0003CD344 /* View */ = { + isa = PBXGroup; + children = ( + E43912D326788CC3003CD344 /* ImageBarButton.swift */, + E43912C626776D8F003CD344 /* LabelView.swift */, + E43912F5267CAE01003CD344 /* TopMenuView.swift */, + E43912F7267CB3D7003CD344 /* MultipleLineInputStackView.swift */, + ); + path = View; + sourceTree = ""; + }; + E43912CA267772D3003CD344 /* View */ = { + isa = PBXGroup; + children = ( + E46E11232677391B005375A1 /* LabelTableViewCell.swift */, + ); + path = View; + sourceTree = ""; + }; + E43912CB267772DA003CD344 /* Controller */ = { + isa = PBXGroup; + children = ( + E42AD11F267093430071B436 /* LabelViewController.swift */, + E43912DF2678AD0A003CD344 /* LabelTableViewDatasource.swift */, + E43912EF267C3EED003CD344 /* LabelControlViewController.swift */, + ); + path = Controller; + sourceTree = ""; + }; + E43912CC267772E4003CD344 /* Model */ = { + isa = PBXGroup; + children = ( + E43912CF267878BF003CD344 /* HexColorConvertable.swift */, + E43912CD26777310003CD344 /* HexColorConverter.swift */, + E43912D1267878F4003CD344 /* HexColorCode.swift */, + E43912D926789E16003CD344 /* Label.swift */, + E43912E7267A1FE5003CD344 /* NewLabelDTO.swift */, + ); + path = Model; + sourceTree = ""; + }; + E496956A2680B3FE001AEB89 /* IssueEditing */ = { + isa = PBXGroup; + children = ( + E49695662680B0B7001AEB89 /* IssueEditViewController.swift */, + E49695772682C4B0001AEB89 /* AdditionalInfoViewController.swift */, + E496958A26830B81001AEB89 /* Protocol */, + E496959A268334BC001AEB89 /* CommonInfoTableDatasource.swift */, + E49695872682FDC9001AEB89 /* CellSelectionTableDelegate.swift */, + ); + path = IssueEditing; + sourceTree = ""; + }; + E496956C26819042001AEB89 /* IssueEditing */ = { + isa = PBXGroup; + children = ( + E496956D2681905A001AEB89 /* IssueInfoView.swift */, + E496957D2682D8D1001AEB89 /* LabelInfoTableViewCell.swift */, + E496958B26830DCE001AEB89 /* MilestoneInfoTableViewCell.swift */, + E4969591268318A4001AEB89 /* AssigneeInfoTableViewCell.swift */, + ); + path = IssueEditing; + sourceTree = ""; + }; + E496958A26830B81001AEB89 /* Protocol */ = { + isa = PBXGroup; + children = ( + E49695812682DCEC001AEB89 /* AdditionalInfoTableDatasource.swift */, + E49695852682FD95001AEB89 /* AdditionalInfoTableDelegate.swift */, + ); + path = Protocol; + sourceTree = ""; + }; FA8840422671F9B000ECCF7A /* Apple */ = { isa = PBXGroup; children = ( @@ -240,9 +428,38 @@ path = Apple; sourceTree = ""; }; + FAB2C36626789ADD009F879C /* Model */ = { + isa = PBXGroup; + children = ( + FA733A2D267C3D77005A40C8 /* Milestone.swift */, + FA431FFA267F3B680010EA91 /* NewMileStoneDTO.swift */, + FAB2C36B2679F6CC009F879C /* MileStoneLabelView.swift */, + ); + path = Model; + sourceTree = ""; + }; + FAB2C36726789AE3009F879C /* View */ = { + isa = PBXGroup; + children = ( + FAB2C36926789D40009F879C /* MileStoneTableViewCell.swift */, + ); + path = View; + sourceTree = ""; + }; + FAB2C36826789AE6009F879C /* Controller */ = { + isa = PBXGroup; + children = ( + E42AD121267093590071B436 /* MileStoneViewController.swift */, + FA733A2F267C3E9E005A40C8 /* MilestoneTableViewDataSource.swift */, + FA431FF8267F36FA0010EA91 /* MileStoneControlViewController.swift */, + ); + path = Controller; + sourceTree = ""; + }; FAC6D8922671E1A800A9E5F9 /* Github */ = { isa = PBXGroup; children = ( + E43912DB2678A737003CD344 /* JWT.swift */, E426DAD026730DC50069E77D /* OAuthResponseDTO.swift */, FAC6D88A2671D5A100A9E5F9 /* GithubAuthorizationManager.swift */, ); @@ -252,12 +469,47 @@ FAEB30B22670B75200C17BE9 /* Network */ = { isa = PBXGroup; children = ( + E43912D726789A83003CD344 /* EndPoint.swift */, + E43912DD2678A76D003CD344 /* RequestKeys.swift */, + E43912E9267A2351003CD344 /* NetworkError.swift */, + E43912D526789812003CD344 /* RequestManager.swift */, FAEB30B32670B76200C17BE9 /* NetworkManager.swift */, E4F7E8E92673394300CE51C2 /* ImageLoadManager.swift */, ); path = Network; sourceTree = ""; }; + FAF927A52680D9560017F8DE /* Model */ = { + isa = PBXGroup; + children = ( + FAF927AE2681DA810017F8DE /* Issue.swift */, + E496959826832B5A001AEB89 /* NewIssue.swift */, + E496958F268317C1001AEB89 /* User.swift */, + ); + path = Model; + sourceTree = ""; + }; + FAF927A62680D95B0017F8DE /* View */ = { + isa = PBXGroup; + children = ( + FAF927AC2680DCCA0017F8DE /* IssueTableViewCell.swift */, + E496956C26819042001AEB89 /* IssueEditing */, + ); + path = View; + sourceTree = ""; + }; + FAF927A72680D9630017F8DE /* Controller */ = { + isa = PBXGroup; + children = ( + E42AD11D267093320071B436 /* IssueViewController.swift */, + E496959626832693001AEB89 /* IssueTableViewDelegate.swift */, + FAF927AA2680DB1C0017F8DE /* IssueTableViewDataSource.swift */, + FAF927B026831A570017F8DE /* IssueDetailViewController.swift */, + E496956A2680B3FE001AEB89 /* IssueEditing */, + ); + path = Controller; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -276,6 +528,7 @@ name = "issue-tracker"; packageProductDependencies = ( FAEB30B02670B74000C17BE9 /* Alamofire */, + E49695752681E7B7001AEB89 /* Down */, ); productName = "issue-tracker"; productReference = E42AD0FA266E52FB0071B436 /* issue-tracker.app */; @@ -306,6 +559,7 @@ mainGroup = E42AD0F1266E52FB0071B436; packageReferences = ( FAEB30AF2670B74000C17BE9 /* XCRemoteSwiftPackageReference "Alamofire" */, + E49695742681E7B7001AEB89 /* XCRemoteSwiftPackageReference "Down" */, ); productRefGroup = E42AD0FB266E52FB0071B436 /* Products */; projectDirPath = ""; @@ -334,32 +588,78 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E496959926832B5A001AEB89 /* NewIssue.swift in Sources */, + FAB2C36A26789D41009F879C /* MileStoneTableViewCell.swift in Sources */, + FAF927AB2680DB1C0017F8DE /* IssueTableViewDataSource.swift in Sources */, E426DAD126730DC50069E77D /* OAuthResponseDTO.swift in Sources */, + E496957C2682D2CE001AEB89 /* CommonDTO.swift in Sources */, + E49695822682DCEC001AEB89 /* AdditionalInfoTableDatasource.swift in Sources */, + FAFD11B1268078F80064AED4 /* CommonTableDelegate.swift in Sources */, + E43912E8267A1FE5003CD344 /* NewLabelDTO.swift in Sources */, FAEB30B42670B76200C17BE9 /* NetworkManager.swift in Sources */, E426DABC2670EC5E0069E77D /* LoginInfo.swift in Sources */, + E49695672680B0B7001AEB89 /* IssueEditViewController.swift in Sources */, + E43912CE26777310003CD344 /* HexColorConverter.swift in Sources */, + FA431FFB267F3B680010EA91 /* NewMileStoneDTO.swift in Sources */, E42AD1182670886F0071B436 /* IssueTrackerTabBarController.swift in Sources */, + E43912F0267C3EED003CD344 /* LabelControlViewController.swift in Sources */, + E43912DE2678A76D003CD344 /* RequestKeys.swift in Sources */, E42AD102266E52FB0071B436 /* LoginViewController.swift in Sources */, + E43912F8267CB3D7003CD344 /* MultipleLineInputStackView.swift in Sources */, + E46E11242677391B005375A1 /* LabelTableViewCell.swift in Sources */, + E43912D0267878BF003CD344 /* HexColorConvertable.swift in Sources */, FAC6D88D2671D8A100A9E5F9 /* SocialLoginManagable.swift in Sources */, E42AD124267093700071B436 /* MyAccountViewController.swift in Sources */, FAEB30AC266F5E9700C17BE9 /* Colors.swift in Sources */, - E42AD122267093590071B436 /* MilestoneViewController.swift in Sources */, + E43912C726776D8F003CD344 /* LabelView.swift in Sources */, + E43912E02678AD0A003CD344 /* LabelTableViewDatasource.swift in Sources */, + FA431FF9267F36FA0010EA91 /* MileStoneControlViewController.swift in Sources */, + E43912EA267A2351003CD344 /* NetworkError.swift in Sources */, + FAF927B126831A570017F8DE /* IssueDetailViewController.swift in Sources */, + E43912F6267CAE01003CD344 /* TopMenuView.swift in Sources */, + E42AD122267093590071B436 /* MileStoneViewController.swift in Sources */, + E496956E2681905A001AEB89 /* IssueInfoView.swift in Sources */, + E496957A2682CB33001AEB89 /* UITableCellExtension.swift in Sources */, + FA733A2E267C3D77005A40C8 /* Milestone.swift in Sources */, E4F7E8EC26733A9700CE51C2 /* IssueTrackerTabBarCreator.swift in Sources */, + E43912EC267B1A4E003CD344 /* LoginInfoDTO.swift in Sources */, E426DAC22671B1C60069E77D /* LoginKeyChainManager.swift in Sources */, + E43912D426788CC3003CD344 /* ImageBarButton.swift in Sources */, + E43912D2267878F4003CD344 /* HexColorCode.swift in Sources */, + FA431FFD267F61150010EA91 /* String.swift in Sources */, + FAB2C36C2679F6CC009F879C /* MileStoneLabelView.swift in Sources */, E426DAC72671F6A00069E77D /* LoginError.swift in Sources */, + E43912D626789812003CD344 /* RequestManager.swift in Sources */, E426DAC42671D95D0069E77D /* LoginService.swift in Sources */, - E426DACB267206D10069E77D /* LoginInfoContainer.swift in Sources */, E42AD120267093430071B436 /* LabelViewController.swift in Sources */, + E43912D826789A83003CD344 /* EndPoint.swift in Sources */, E426DABE2670EC830069E77D /* SocialLoginManagerDelegate.swift in Sources */, + E4969590268317C1001AEB89 /* User.swift in Sources */, + E496958C26830DCE001AEB89 /* MilestoneInfoTableViewCell.swift in Sources */, + FA733A30267C3E9E005A40C8 /* MilestoneTableViewDataSource.swift in Sources */, + E49695782682C4B0001AEB89 /* AdditionalInfoViewController.swift in Sources */, E42AD0FE266E52FB0071B436 /* AppDelegate.swift in Sources */, + FAF927AF2681DA810017F8DE /* Issue.swift in Sources */, + E43912EE267C3C0E003CD344 /* CellAction.swift in Sources */, + E49695862682FD95001AEB89 /* AdditionalInfoTableDelegate.swift in Sources */, E4F7E8E826732B6300CE51C2 /* TabBarChildInfo.swift in Sources */, FAC6D88B2671D5A100A9E5F9 /* GithubAuthorizationManager.swift in Sources */, E4F7E8EA2673394300CE51C2 /* ImageLoadManager.swift in Sources */, E42AD11E267093320071B436 /* IssueViewController.swift in Sources */, + E43912F4267CAA8D003CD344 /* UITextFieldExtension.swift in Sources */, + E49695882682FDC9001AEB89 /* CellSelectionTableDelegate.swift in Sources */, + E43912DC2678A737003CD344 /* JWT.swift in Sources */, + E4969592268318A4001AEB89 /* AssigneeInfoTableViewCell.swift in Sources */, E426DABA2670EC2D0069E77D /* AppleAuthorizationManager.swift in Sources */, FAEB30AE266F6A7800C17BE9 /* UIImageExtension.swift in Sources */, + E496959726832693001AEB89 /* IssueTableViewDelegate.swift in Sources */, E42AD12626709C010071B436 /* UIViewControllerExtension.swift in Sources */, + E496959B268334BC001AEB89 /* CommonInfoTableDatasource.swift in Sources */, E42AD100266E52FB0071B436 /* SceneDelegate.swift in Sources */, + E43912DA26789E16003CD344 /* Label.swift in Sources */, + E496957E2682D8D1001AEB89 /* LabelInfoTableViewCell.swift in Sources */, E426DAC92671F7760069E77D /* AlertFactory.swift in Sources */, + FAF927AD2680DCCA0017F8DE /* IssueTableViewCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -565,6 +865,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + E49695742681E7B7001AEB89 /* XCRemoteSwiftPackageReference "Down" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/johnxnguyen/Down"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.11.0; + }; + }; FAEB30AF2670B74000C17BE9 /* XCRemoteSwiftPackageReference "Alamofire" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/Alamofire.git"; @@ -576,6 +884,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + E49695752681E7B7001AEB89 /* Down */ = { + isa = XCSwiftPackageProductDependency; + package = E49695742681E7B7001AEB89 /* XCRemoteSwiftPackageReference "Down" */; + productName = Down; + }; FAEB30B02670B74000C17BE9 /* Alamofire */ = { isa = XCSwiftPackageProductDependency; package = FAEB30AF2670B74000C17BE9 /* XCRemoteSwiftPackageReference "Alamofire" */; diff --git a/iOS/issue-tracker/issue-tracker.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/iOS/issue-tracker/issue-tracker.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 20e547a35..3a1719636 100644 --- a/iOS/issue-tracker/issue-tracker.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/iOS/issue-tracker/issue-tracker.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -9,6 +9,15 @@ "revision": "f96b619bcb2383b43d898402283924b80e2c4bae", "version": "5.4.3" } + }, + { + "package": "Down", + "repositoryURL": "https://github.com/johnxnguyen/Down", + "state": { + "branch": null, + "revision": "f34b166be1f1db4aa8f573067e901d72f2a6be57", + "version": "0.11.0" + } } ] }, diff --git a/iOS/issue-tracker/issue-tracker/Assets.xcassets/AddIssueBtn.colorset/Contents.json b/iOS/issue-tracker/issue-tracker/Assets.xcassets/AddIssueBtn.colorset/Contents.json new file mode 100644 index 000000000..805e83152 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Assets.xcassets/AddIssueBtn.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.478", + "red" : "0.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.478", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/issue-tracker/issue-tracker/Assets.xcassets/CloseMileStoneBG.colorset/Contents.json b/iOS/issue-tracker/issue-tracker/Assets.xcassets/CloseMileStoneBG.colorset/Contents.json new file mode 100644 index 000000000..0cb94da62 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Assets.xcassets/CloseMileStoneBG.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.831", + "red" : "0.800" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.831", + "red" : "0.800" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/issue-tracker/issue-tracker/Assets.xcassets/CloseMileStoneTint.colorset/Contents.json b/iOS/issue-tracker/issue-tracker/Assets.xcassets/CloseMileStoneTint.colorset/Contents.json new file mode 100644 index 000000000..85ef47f86 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Assets.xcassets/CloseMileStoneTint.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.906", + "green" : "0.145", + "red" : "0.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.906", + "green" : "0.145", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/issue-tracker/issue-tracker/Assets.xcassets/Description.colorset/Contents.json b/iOS/issue-tracker/issue-tracker/Assets.xcassets/Description.colorset/Contents.json new file mode 100644 index 000000000..7fec4023b --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Assets.xcassets/Description.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.576", + "green" : "0.557", + "red" : "0.557" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.576", + "green" : "0.557", + "red" : "0.557" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/issue-tracker/issue-tracker/Assets.xcassets/MileStoneSuccess.colorset/Contents.json b/iOS/issue-tracker/issue-tracker/Assets.xcassets/MileStoneSuccess.colorset/Contents.json new file mode 100644 index 000000000..d3187d8b9 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Assets.xcassets/MileStoneSuccess.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.349", + "green" : "0.780", + "red" : "0.204" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.349", + "green" : "0.780", + "red" : "0.204" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/issue-tracker/issue-tracker/Assets.xcassets/OpenMileStoneBG.colorset/Contents.json b/iOS/issue-tracker/issue-tracker/Assets.xcassets/OpenMileStoneBG.colorset/Contents.json new file mode 100644 index 000000000..4f0dc6688 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Assets.xcassets/OpenMileStoneBG.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.922", + "red" : "0.780" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.922", + "red" : "0.780" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/issue-tracker/issue-tracker/Assets.xcassets/OpenMileStoneTint.colorset/Contents.json b/iOS/issue-tracker/issue-tracker/Assets.xcassets/OpenMileStoneTint.colorset/Contents.json new file mode 100644 index 000000000..805e83152 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Assets.xcassets/OpenMileStoneTint.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.478", + "red" : "0.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.478", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/issue-tracker/issue-tracker/Common/Colors.swift b/iOS/issue-tracker/issue-tracker/Common/Colors.swift index a0133edc7..4721a689e 100644 --- a/iOS/issue-tracker/issue-tracker/Common/Colors.swift +++ b/iOS/issue-tracker/issue-tracker/Common/Colors.swift @@ -14,4 +14,10 @@ enum Colors { static let deleteSunnyside = UIColor(named: "DeleteSunnyside") ?? UIColor.yellow static let openBrocoli = UIColor(named: "OpenBrocoli") ?? UIColor.purple static let mainGrape = UIColor(named: "MainGrape") ?? UIColor.purple + static let mileStoneSuceess = UIColor(named: "MileStoneSuccess") ?? UIColor.green + static let description = UIColor(named: "Description") ?? UIColor.systemGray2 + static let openMileStoneTint = UIColor(named: "OpenMileStoneTint") ?? UIColor.black + static let openMileStoneBG = UIColor(named: "OpenMileStoneBG") ?? UIColor.purple + static let closeMileStoneTint = UIColor(named: "CloseMileStoneTint") ?? UIColor.black + static let closeMileStoneBG = UIColor(named: "CloseMileStoneBG") ?? UIColor.blue } diff --git a/iOS/issue-tracker/issue-tracker/Common/String.swift b/iOS/issue-tracker/issue-tracker/Common/String.swift new file mode 100644 index 000000000..84f8d3a97 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Common/String.swift @@ -0,0 +1,25 @@ +// +// String.swift +// issue-tracker +// +// Created by jinseo park on 6/20/21. +// + +import Foundation + +extension String { + enum ValidityType { + case date + } + + func isValid(_ validityType: ValidityType)-> Bool { + let format = "SELF MATCHES %@" + var regex = "" + + switch validityType { + case .date: + regex = "[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1]) (2[0-3]|[01][0-9]):[0-5][0-9]" + } + return NSPredicate(format: format, regex).evaluate(with: self) + } +} diff --git a/iOS/issue-tracker/issue-tracker/Common/UITableCellExtension.swift b/iOS/issue-tracker/issue-tracker/Common/UITableCellExtension.swift new file mode 100644 index 000000000..12288e24c --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Common/UITableCellExtension.swift @@ -0,0 +1,15 @@ +// +// UITableCellExtension.swift +// issue-tracker +// +// Created by Song on 2021/06/23. +// + +import UIKit + +extension UITableViewCell { + static var reuseID: String { + return String(describing: self) + } +} + diff --git a/iOS/issue-tracker/issue-tracker/Common/UITextFieldExtension.swift b/iOS/issue-tracker/issue-tracker/Common/UITextFieldExtension.swift new file mode 100644 index 000000000..7dca84baa --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Common/UITextFieldExtension.swift @@ -0,0 +1,18 @@ +// +// UITextFieldExtension.swift +// issue-tracker +// +// Created by Song on 2021/06/18. +// + +import UIKit + +extension UITextField { + func isEmpty() -> Bool { + if let text = self.text, text.count > 0 { + return false + } else { + return true + } + } +} diff --git a/iOS/issue-tracker/issue-tracker/LifeCycle/SceneDelegate.swift b/iOS/issue-tracker/issue-tracker/LifeCycle/SceneDelegate.swift index 91d2b9003..bea57751c 100644 --- a/iOS/issue-tracker/issue-tracker/LifeCycle/SceneDelegate.swift +++ b/iOS/issue-tracker/issue-tracker/LifeCycle/SceneDelegate.swift @@ -11,17 +11,19 @@ import AuthenticationServices class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? + private let loginInfo = LoginInfo.shared func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { var loginManager: LoginKeyChainManager? - var loginInfo: LoginInfo? + var loginInfo: LoginInfoDTO? for loginService in LoginService.allCases { loginManager = LoginKeyChainManager(loginService: loginService) loginInfo = loginManager?.read() if loginInfo != nil { + self.loginInfo.service = loginService break } } @@ -43,9 +45,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } - private func straightToIssueTrackerScene(with loginManager: LoginKeyChainManager,_ loginInfo: LoginInfo) { + private func straightToIssueTrackerScene(with loginManager: LoginKeyChainManager,_ loginInfoDTO: LoginInfoDTO) { + loginInfo.store(loginInfoDTO: loginInfoDTO) + DispatchQueue.main.async { - let issueTrackerTabBarControllerCreator = IssueTrackerTabBarCreator(loginInfo: loginInfo) + let issueTrackerTabBarControllerCreator = IssueTrackerTabBarCreator() let issueTrackerTabBarController = issueTrackerTabBarControllerCreator.create() self.window?.rootViewController = issueTrackerTabBarController } diff --git a/iOS/issue-tracker/issue-tracker/Login/Controller/LoginViewController.swift b/iOS/issue-tracker/issue-tracker/Login/Controller/LoginViewController.swift index d451275d7..7545b1bd6 100644 --- a/iOS/issue-tracker/issue-tracker/Login/Controller/LoginViewController.swift +++ b/iOS/issue-tracker/issue-tracker/Login/Controller/LoginViewController.swift @@ -81,7 +81,6 @@ class LoginViewController: UIViewController { let button = socialLoginButton(with: image, title) button.addTarget(self, action: #selector(loginWithGithubTouched), for: .touchUpInside) return button - }() private lazy var appleLoginButton: UIButton = { @@ -96,6 +95,7 @@ class LoginViewController: UIViewController { private let borderWidth: CGFloat = 1 private var socialLoginManager: SocialLoginManagable? + private let loginInfo = LoginInfo.shared override func viewDidLoad() { super.viewDidLoad() @@ -187,8 +187,8 @@ class LoginViewController: UIViewController { ]) } - private func presentIssueViewController(with loginInfo: LoginInfo) { - let issueTrackerTabBarControllerCreator = IssueTrackerTabBarCreator(loginInfo: loginInfo) + private func presentIssueViewController() { + let issueTrackerTabBarControllerCreator = IssueTrackerTabBarCreator() let issueTrackerTabBarController = issueTrackerTabBarControllerCreator.create() issueTrackerTabBarController.modalPresentationStyle = .fullScreen @@ -206,32 +206,42 @@ class LoginViewController: UIViewController { } @objc private func loginWithGithubTouched(_ sender: UIButton) { - configureLoginManager(type: .github) + configureLoginManager(service: .github) socialLoginManager?.login() } @objc private func loginWithAppleTouched(_ sender: UIButton) { - configureLoginManager(type: .apple) + configureLoginManager(service: .apple) socialLoginManager?.login() } - private func configureLoginManager(type: LoginService) { - let keyChainManager = LoginKeyChainManager(loginService: type) - let loginManager = GithubAuthorizationManager(viewController: self, - delegate: self, - keyChainSaver: keyChainManager) - self.socialLoginManager = loginManager + private func configureLoginManager(service: LoginService) { + let keyChainManager = LoginKeyChainManager(loginService: service) + loginInfo.service = service + + switch service { + case .github: + socialLoginManager = GithubAuthorizationManager(viewController: self, + delegate: self, + keyChainSaver: keyChainManager) + case .apple: + socialLoginManager = AppleAuthorizationManager(viewController: self, + delegate: self, + keyChainSaver: keyChainManager) + } } - } extension LoginViewController: SocialLoginManagerDelegate { - func didSocialLoginSuccess(with loginInfo: LoginInfo) { - presentIssueViewController(with: loginInfo) + func didSocialLoginSuccess(with loginInfoDTO: LoginInfoDTO) { + let loginInfo = LoginInfo.shared + loginInfo.store(loginInfoDTO: loginInfoDTO) + presentIssueViewController() } func didSocialLoginFail(with error: LoginError) { let errorText = error.description presentAlert(with: errorText) } + } diff --git a/iOS/issue-tracker/issue-tracker/Login/Model/Apple/AppleAuthorizationManager.swift b/iOS/issue-tracker/issue-tracker/Login/Model/Apple/AppleAuthorizationManager.swift index 450141932..368356ae5 100644 --- a/iOS/issue-tracker/issue-tracker/Login/Model/Apple/AppleAuthorizationManager.swift +++ b/iOS/issue-tracker/issue-tracker/Login/Model/Apple/AppleAuthorizationManager.swift @@ -8,7 +8,7 @@ import AuthenticationServices final class AppleAuthorizationManager: NSObject, SocialLoginManagable { - + private weak var viewController: UIViewController? private weak var delegate: SocialLoginManagerDelegate? private var keyChainSaver: LoginKeyChainManager @@ -42,10 +42,11 @@ extension AppleAuthorizationManager: ASAuthorizationControllerDelegate { } let userID = appleIDCredential.user - let loginInfo = LoginInfo(userID: userID, jwt: tokenInString, avatarURL: nil, name: name) + let jwt = JWT(jwt: tokenInString, tokenType: "Bearer") + let loginInfoDTO = LoginInfoDTO(userID: userID, jwt: jwt, avatarURL: nil, name: name) - if keyChainSaver.save(loginInfo) { - delegate?.didSocialLoginSuccess(with: loginInfo) + if keyChainSaver.save(loginInfoDTO) { + delegate?.didSocialLoginSuccess(with: loginInfoDTO) } else { let saveError = LoginError.keyChainSave delegate?.didSocialLoginFail(with: saveError) diff --git a/iOS/issue-tracker/issue-tracker/Login/Model/Github/GithubAuthorizationManager.swift b/iOS/issue-tracker/issue-tracker/Login/Model/Github/GithubAuthorizationManager.swift index 9253fed74..c59312a97 100644 --- a/iOS/issue-tracker/issue-tracker/Login/Model/Github/GithubAuthorizationManager.swift +++ b/iOS/issue-tracker/issue-tracker/Login/Model/Github/GithubAuthorizationManager.swift @@ -51,24 +51,28 @@ extension GithubAuthorizationManager: SocialLoginManagable { guard error == nil, let successURL = callbackURL else { return } + let codeKey = Parameter.code.key() + guard let code = NSURLComponents(string: (successURL.absoluteString))? - .queryItems?.filter({$0.name == "code"}) + .queryItems?.filter({$0.name == codeKey}) .first? .value else { return } - let networkmanager = NetworkManager() + let parameter = [codeKey: code] + let networkmanager = NetworkManager(baseAddress: EndPoint.baseAddress) + let OAuthEndpoint = EndPoint.OAuth.path() - networkmanager.setInfoGithub(with: code) { [weak self] (result: Result) in + networkmanager.get(endpoint: OAuthEndpoint, queryParameters: parameter) { [weak self] (result: Result) in guard let self = self else { return } switch result { case .success(let response): - let loginInfo = LoginInfo(userID: nil, - jwt: response.jwt.jwt, + let loginInfoDTO = LoginInfoDTO(userID: nil, + jwt: response.jwt, avatarURL: response.avatarUrl, name: response.loginId) - if self.keyChainSaver.save(loginInfo) { - self.delegate?.didSocialLoginSuccess(with: loginInfo) + if self.keyChainSaver.save(loginInfoDTO) { + self.delegate?.didSocialLoginSuccess(with: loginInfoDTO) } else { let saveError = LoginError.keyChainSave self.delegate?.didSocialLoginFail(with: saveError) diff --git a/iOS/issue-tracker/issue-tracker/Login/Model/Github/JWT.swift b/iOS/issue-tracker/issue-tracker/Login/Model/Github/JWT.swift new file mode 100644 index 000000000..5e6ee1918 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Login/Model/Github/JWT.swift @@ -0,0 +1,24 @@ +// +// JWT.swift +// issue-tracker +// +// Created by Song on 2021/06/15. +// + +import Foundation + +struct JWT: Codable { + let jwt: String + let tokenType: String + + enum CodingKeys: String, CodingKey { + case jwt + case tokenType + } +} + +extension JWT: CustomStringConvertible { + var description: String { + return "\(tokenType) \(jwt)" + } +} diff --git a/iOS/issue-tracker/issue-tracker/Login/Model/Github/OAuthResponseDTO.swift b/iOS/issue-tracker/issue-tracker/Login/Model/Github/OAuthResponseDTO.swift index 28048bccb..1b0e0cc5e 100644 --- a/iOS/issue-tracker/issue-tracker/Login/Model/Github/OAuthResponseDTO.swift +++ b/iOS/issue-tracker/issue-tracker/Login/Model/Github/OAuthResponseDTO.swift @@ -7,17 +7,7 @@ import Foundation -struct JWT: Decodable { - let jwt: String - let tokenType: String - - enum CodingKeys: String, CodingKey { - case jwt - case tokenType - } -} - -struct OAuthResponseDTO: Decodable { +struct OAuthResponseDTO: Codable { let jwt: JWT let avatarUrl: String let loginId: String diff --git a/iOS/issue-tracker/issue-tracker/Login/Model/LoginError.swift b/iOS/issue-tracker/issue-tracker/Login/Model/LoginError.swift index 49f68f139..c186bd31c 100644 --- a/iOS/issue-tracker/issue-tracker/Login/Model/LoginError.swift +++ b/iOS/issue-tracker/issue-tracker/Login/Model/LoginError.swift @@ -24,7 +24,7 @@ extension LoginError: CustomStringConvertible { case .keyChainSave: return "로그인 정보 저장에 실패했습니다. \n로그인을 다시 시도해주세요." case .logout: - return "로그아웃 기능은 제공되지 않습니다🥺" + return "로그아웃에 실패했습니다🙉" } } } diff --git a/iOS/issue-tracker/issue-tracker/Login/Model/LoginInfo.swift b/iOS/issue-tracker/issue-tracker/Login/Model/LoginInfo.swift index 401c83dcf..eb94327e6 100644 --- a/iOS/issue-tracker/issue-tracker/Login/Model/LoginInfo.swift +++ b/iOS/issue-tracker/issue-tracker/Login/Model/LoginInfo.swift @@ -7,9 +7,29 @@ import Foundation -struct LoginInfo: Codable { - let userID: String? - let jwt: String - let avatarURL: String? - let name: String +class LoginInfo { + static let shared = LoginInfo() + + var service: LoginService? + var userID: String? + var jwt: JWT? + var avatarURL: String? + var name: String? + + private init() {} + + func store(loginInfoDTO: LoginInfoDTO) { + userID = loginInfoDTO.userID + jwt = loginInfoDTO.jwt + avatarURL = loginInfoDTO.avatarURL + name = loginInfoDTO.name + } + + func clear() { + service = nil + userID = nil + jwt = nil + avatarURL = nil + name = nil + } } diff --git a/iOS/issue-tracker/issue-tracker/Login/Model/LoginInfoDTO.swift b/iOS/issue-tracker/issue-tracker/Login/Model/LoginInfoDTO.swift new file mode 100644 index 000000000..34497cdeb --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Login/Model/LoginInfoDTO.swift @@ -0,0 +1,15 @@ +// +// LoginInfoDTO.swift +// issue-tracker +// +// Created by Song on 2021/06/17. +// + +import Foundation + +struct LoginInfoDTO: Codable { + let userID: String? + let jwt: JWT + let avatarURL: String? + let name: String +} diff --git a/iOS/issue-tracker/issue-tracker/Login/Model/LoginKeyChainManager.swift b/iOS/issue-tracker/issue-tracker/Login/Model/LoginKeyChainManager.swift index 2abf2a873..e8cb46fdd 100644 --- a/iOS/issue-tracker/issue-tracker/Login/Model/LoginKeyChainManager.swift +++ b/iOS/issue-tracker/issue-tracker/Login/Model/LoginKeyChainManager.swift @@ -16,8 +16,8 @@ final class LoginKeyChainManager { self.loginService = loginService.description } - func save(_ loginInfo: LoginInfo) -> Bool { - guard let loginInfo = try? JSONEncoder().encode(loginInfo) else { return false } + func save(_ loginInfoDTO: LoginInfoDTO) -> Bool { + guard let loginInfo = try? JSONEncoder().encode(loginInfoDTO) else { return false } let saveQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrService: loginService, @@ -26,7 +26,7 @@ final class LoginKeyChainManager { return SecItemAdd(saveQuery as CFDictionary, nil) == errSecSuccess } - func read() -> LoginInfo? { + func read() -> LoginInfoDTO? { let readQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrService: loginService, kSecMatchLimit: kSecMatchLimitOne, @@ -38,9 +38,9 @@ final class LoginKeyChainManager { guard SecItemCopyMatching(readQuery as CFDictionary, &item) == errSecSuccess, let existingItem = item as? [String: Any], let data = existingItem[kSecAttrGeneric as String] as? Data, - let loginInfo = try? JSONDecoder().decode(LoginInfo.self, from: data) else { return nil } + let loginInfoDTO = try? JSONDecoder().decode(LoginInfoDTO.self, from: data) else { return nil } - return loginInfo + return loginInfoDTO } func delete() -> Bool { diff --git a/iOS/issue-tracker/issue-tracker/Login/Model/Protocol/SocialLoginManagerDelegate.swift b/iOS/issue-tracker/issue-tracker/Login/Model/Protocol/SocialLoginManagerDelegate.swift index ceb9e3087..a6febb029 100644 --- a/iOS/issue-tracker/issue-tracker/Login/Model/Protocol/SocialLoginManagerDelegate.swift +++ b/iOS/issue-tracker/issue-tracker/Login/Model/Protocol/SocialLoginManagerDelegate.swift @@ -8,6 +8,6 @@ import Foundation protocol SocialLoginManagerDelegate: AnyObject { - func didSocialLoginSuccess(with loginInfo: LoginInfo) + func didSocialLoginSuccess(with loginInfo: LoginInfoDTO) func didSocialLoginFail(with error: LoginError) } diff --git a/iOS/issue-tracker/issue-tracker/Main/Common/CellAction.swift b/iOS/issue-tracker/issue-tracker/Main/Common/CellAction.swift new file mode 100644 index 000000000..406b3340e --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Common/CellAction.swift @@ -0,0 +1,25 @@ +// +// CellAction.swift +// issue-tracker +// +// Created by Song on 2021/06/18. +// + +import Foundation + +enum CellAction { + case delete + case edit + case close + + func buttonTitle() -> String { + switch self { + case .delete: + return "삭제" + case .edit: + return "수정" + case .close: + return "닫기" + } + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Common/CommonDTO.swift b/iOS/issue-tracker/issue-tracker/Main/Common/CommonDTO.swift new file mode 100644 index 000000000..d636cf750 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Common/CommonDTO.swift @@ -0,0 +1,13 @@ +// +// CommonDTO.swift +// issue-tracker +// +// Created by Song on 2021/06/23. +// + +import Foundation + +struct CommonDTO: Decodable { + let data: [T]? + let error: String? +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Common/CommonTableDelegate.swift b/iOS/issue-tracker/issue-tracker/Main/Common/CommonTableDelegate.swift new file mode 100644 index 000000000..5204b49a2 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Common/CommonTableDelegate.swift @@ -0,0 +1,40 @@ +// +// CommonTableDelegate.swift +// issue-tracker +// +// Created by jinseo park on 6/21/21. +// + +import UIKit + +final class CommonTableDelegate: NSObject, UITableViewDelegate { + + typealias CellActionHandler = (Int, CellAction) -> Void + private var cellActionHandler: CellActionHandler + private var cellHeight: CGFloat + + init(cellActionHandler: @escaping CellActionHandler, cellHeight: CGFloat) { + self.cellActionHandler = cellActionHandler + self.cellHeight = cellHeight + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return self.cellHeight + } + + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + let deleteAction = UIContextualAction(style: .destructive, + title: CellAction.delete.buttonTitle()) { [weak self] _, _, _ in + self?.cellActionHandler(indexPath.row, .delete) + } + deleteAction.image = UIImage(systemName: "trash") + + let editAction = UIContextualAction(style: .normal, + title: CellAction.edit.buttonTitle()) { [weak self] _, _, _ in + self?.cellActionHandler(indexPath.row, .edit) + } + editAction.image = UIImage(systemName: "pencil") + + return UISwipeActionsConfiguration(actions: [deleteAction, editAction]) + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Common/View/ImageBarButton.swift b/iOS/issue-tracker/issue-tracker/Main/Common/View/ImageBarButton.swift new file mode 100644 index 000000000..0ecba52f7 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Common/View/ImageBarButton.swift @@ -0,0 +1,50 @@ +// +// AddButton.swift +// issue-tracker +// +// Created by Song on 2021/06/15. +// + +import UIKit + +final class ImageBarButton: UIButton { + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } + + override var isEnabled: Bool { + didSet { + self.tintColor = self.isEnabled ? Colors.mainGrape : UIColor.lightGray + } + } + + private func configure() { + tintColor = Colors.mainGrape + setTitleColor(Colors.mainGrape, for: .normal) + setTitleColor(UIColor.lightGray, for: .disabled) + semanticContentAttribute = .forceRightToLeft + imageEdgeInsets = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 0) + } + + func configure(with systemImageName: String,_ buttonTitle: String) { + setImage(of: systemImageName) + setTitle(buttonTitle, for: .normal) + } + + private func setImage(of systemName: String) { + let image = UIImage(systemName: systemName) + setImage(image, for: .normal) + } + + func moveImageToLeft() { + semanticContentAttribute = .forceLeftToRight + imageEdgeInsets = UIEdgeInsets(top: 0, left: -4, bottom: 0, right: 0) + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Common/View/LabelView.swift b/iOS/issue-tracker/issue-tracker/Main/Common/View/LabelView.swift new file mode 100644 index 000000000..e6c37de57 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Common/View/LabelView.swift @@ -0,0 +1,72 @@ +// +// LabelView.swift +// issue-tracker +// +// Created by Song on 2021/06/14. +// + +import UIKit + +final class LabelView: UIView { + + private lazy var labelTitle: UILabel = { + let label = UILabel() + label.textColor = UIColor.white + label.textAlignment = .center + label.text = "레이블" + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let spacing: CGFloat = 15 + + private lazy var labelHeight: CGFloat = { + return spacing * 2 + }() + + private let colorConverter = HexColorConverter() + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + private func configure() { + layer.cornerRadius = labelHeight * 0.5 + backgroundColor = Colors.mainGrape + translatesAutoresizingMaskIntoConstraints = false + + addLabelTitle() + } + + private func addLabelTitle() { + addSubview(labelTitle) + + NSLayoutConstraint.activate([ + labelTitle.centerXAnchor.constraint(equalTo: centerXAnchor), + labelTitle.centerYAnchor.constraint(equalTo: centerYAnchor), + widthAnchor.constraint(equalTo: labelTitle.widthAnchor, constant: spacing * 1.8), + heightAnchor.constraint(equalToConstant: labelHeight) + ]) + } + + func configure(with backgroundColor: UIColor,_ textColor: UIColor,_ title: String?) { + self.backgroundColor = backgroundColor + labelTitle.textColor = textColor + + if let title = title { + labelTitle.text = title + } + } + + func configure(with hexColorCode: HexColorCode,_ title: String?) { + let backgroundColor = colorConverter.convertHex(hexColorCode) + let textColor = colorConverter.isColorDark(hex: hexColorCode) ? UIColor.white : UIColor.black + configure(with: backgroundColor, textColor, title) + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Common/View/MultipleLineInputStackView.swift b/iOS/issue-tracker/issue-tracker/Main/Common/View/MultipleLineInputStackView.swift new file mode 100644 index 000000000..667dee77b --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Common/View/MultipleLineInputStackView.swift @@ -0,0 +1,113 @@ +// +// ThreeLineInputStackView.swift +// issue-tracker +// +// Created by Song on 2021/06/18. +// + +import UIKit + +struct InputLineItem { + let category: String + let inputView: UIView +} + +final class MultipleLineInputStackView: UIStackView { + + private lazy var lineHeight: CGFloat = { + return frame.height / 3 + }() + + private lazy var elementSpacing: CGFloat = { + return lineHeight / 2 + }() + + private var lastItemLabel: UILabel = { + let label = UILabel() + return label + }() + + private var items: [InputLineItem]? + private let borderColor = Colors.border.cgColor + private let borderWidth: CGFloat = 1 + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init(coder: NSCoder) { + super.init(coder: coder) + configure() + } + + private func configure() { + backgroundColor = UIColor.white + axis = .vertical + distribution = .fillEqually + translatesAutoresizingMaskIntoConstraints = false + } + + func configure(with items: [InputLineItem]) { + self.items = items + + items.enumerated().forEach { index, inputLineItem in + let containerFrame = CGRect(x: 0, y: 0, width: frame.width, height: lineHeight) + let container = UIView(frame: containerFrame) + + let category = inputLineItem.category + + var titleLabel: UILabel + + if index == 2 { + titleLabel = lastItemLabel + }else { + titleLabel = UILabel() + } + + titleLabel.text = category + titleLabel.translatesAutoresizingMaskIntoConstraints = false + + container.addSubview(titleLabel) + NSLayoutConstraint.activate([ + titleLabel.leadingAnchor.constraint(equalTo: container.safeAreaLayoutGuide.leadingAnchor, constant: elementSpacing), + titleLabel.centerYAnchor.constraint(equalTo: container.safeAreaLayoutGuide.centerYAnchor) + ]) + + let inputView = inputLineItem.inputView + inputView.translatesAutoresizingMaskIntoConstraints = false + + container.addSubview(inputView) + NSLayoutConstraint.activate([ + inputView.leadingAnchor.constraint(equalTo: container.safeAreaLayoutGuide.leadingAnchor, constant: elementSpacing * 5), + inputView.trailingAnchor.constraint(equalTo: container.safeAreaLayoutGuide.trailingAnchor, constant: -elementSpacing), + inputView.centerYAnchor.constraint(equalTo: container.safeAreaLayoutGuide.centerYAnchor), + inputView.topAnchor.constraint(equalTo: container.safeAreaLayoutGuide.topAnchor) + ]) + addArrangedSubview(container) + } + addDivisionLines() + } + + func setLabelColor(correct: Bool) { + if correct { + lastItemLabel.textColor = Colors.mainGrape + }else { + lastItemLabel.textColor = UIColor.red + } + } + + private func addDivisionLines() { + guard let itemCount = items?.count, itemCount >= 2 else { return } + + let size = CGSize(width: frame.width - elementSpacing, height: borderWidth) + + for i in 1...itemCount-1 { + let line = CALayer() + let origin = CGPoint(x: elementSpacing, y: lineHeight * CGFloat(i) - borderWidth) + line.frame = CGRect(origin: origin, size: size) + line.backgroundColor = borderColor + layer.addSublayer(line) + } + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Common/View/TopMenuView.swift b/iOS/issue-tracker/issue-tracker/Main/Common/View/TopMenuView.swift new file mode 100644 index 000000000..893b557ad --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Common/View/TopMenuView.swift @@ -0,0 +1,70 @@ +// +// TopMenuView.swift +// issue-tracker +// +// Created by Song on 2021/06/18. +// + +import UIKit + +final class TopMenuView: UIView { + + private lazy var titleLabel: UILabel = { + let titleLabel = UILabel() + titleLabel.text = menuTitle + titleLabel.font = .systemFont(ofSize: 18, weight: .bold) + titleLabel.translatesAutoresizingMaskIntoConstraints = false + return titleLabel + }() + + private var rightButton: UIButton? + private var leftButton: UIButton? + private var menuTitle: String? + private let spacing: CGFloat = 16 + + func configure(withTitle title: String?, rightButton: UIButton?, leftButton: UIButton?) { + self.menuTitle = title + self.rightButton = rightButton + self.leftButton = leftButton + + addTitleLabel() + addRightButton() + addLeftButton() + } + + private func addTitleLabel() { + addSubview(titleLabel) + + NSLayoutConstraint.activate([ + titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor), + titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor) + ]) + } + + private func addRightButton() { + guard let rightButton = rightButton else { return } + + addSubview(rightButton) + + NSLayoutConstraint.activate([ + rightButton.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor), + rightButton.widthAnchor.constraint(greaterThanOrEqualToConstant: spacing * 2), + rightButton.heightAnchor.constraint(equalToConstant: spacing * 1.5), + rightButton.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -spacing) + ]) + } + + private func addLeftButton() { + guard let leftButton = leftButton else { return } + + addSubview(leftButton) + + NSLayoutConstraint.activate([ + leftButton.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor), + leftButton.widthAnchor.constraint(greaterThanOrEqualToConstant: spacing * 2), + leftButton.heightAnchor.constraint(equalToConstant: spacing * 1.5), + leftButton.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: spacing) + ]) + } +} + diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueDetailViewController.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueDetailViewController.swift new file mode 100644 index 000000000..e737c5d12 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueDetailViewController.swift @@ -0,0 +1,27 @@ +// +// IssueDetailViewController.swift +// issue-tracker +// +// Created by jinseo park on 6/23/21. +// + +import Foundation +import UIKit + +class IssueDetailViewController: UIViewController { + + private lazy var backButton: ImageBarButton = { + let button = ImageBarButton() + button.configure(with: "chevron.backward", "목록") + button.addTarget(self, action: #selector(backToIssuesTouched), for: .touchUpInside) + return button + }() + + override func viewDidLoad() { + super.viewDidLoad() + } + + @objc func backToIssuesTouched(_ sender: UIButton) { + self.navigationController?.popViewController(animated: true) + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/AdditionalInfoViewController.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/AdditionalInfoViewController.swift new file mode 100644 index 000000000..e5ea97caf --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/AdditionalInfoViewController.swift @@ -0,0 +1,207 @@ +// +// AdditionalInfoViewController.swift +// issue-tracker +// +// Created by Song on 2021/06/23. +// + +import UIKit + +final class AdditionalInfoViewController: UIViewController { + + private lazy var topMenuView: TopMenuView = { + let topMenuView = TopMenuView() + topMenuView.configure(withTitle: sceneTitle, rightButton: saveButton, leftButton: cancelButton) + topMenuView.translatesAutoresizingMaskIntoConstraints = false + return topMenuView + }() + + private lazy var saveButton: ImageBarButton = { + let button = ImageBarButton() + button.configure(with: "", "저장") + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(saveButtonTouched), for: .touchUpInside) + changeSaveButtonEnableStatus() + return button + }() + + private lazy var cancelButton: ImageBarButton = { + let button = ImageBarButton() + button.configure(with: "chevron.backward", "취소") + button.moveImageToLeft() + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(cancelButtonTouched), for: .touchUpInside) + return button + }() + + private lazy var infoListTable: UITableView = { + let tableView = UITableView() + let cellID = InfoCell.reuseID + tableView.register(InfoCell.self, forCellReuseIdentifier: cellID) + tableView.tintColor = Colors.mainGrape + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.dataSource = tableDatasource + return tableView + }() + + private lazy var singleLineHeight: CGFloat = { + return view.frame.height * 0.05 + }() + + private lazy var spacing: CGFloat = { + return singleLineHeight * 0.5 + }() + + private var sceneTitle: String? + private var infoCell: InfoCell? + private var tableDatasource: CommonInfoTableDatasource? + private var tableDelegate: AdditionalInfoTableDelegate? + + private var selectedInfo = [Info]() + private var saveOperation: (([Info]) -> Void)? + private var endpoint: EndPoint? + private var networkManager: NetworkManagerOperations? + + override func viewDidLoad() { + super.viewDidLoad() + configureView() + loadData() + } + + private func configureView() { + view.backgroundColor = .white + + addTopMenu() + addTableView() + } + + private func addTopMenu() { + view.addSubview(topMenuView) + + NSLayoutConstraint.activate([ + topMenuView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + topMenuView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + topMenuView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: spacing), + topMenuView.heightAnchor.constraint(equalToConstant: singleLineHeight) + ]) + } + + private func addTableView() { + view.addSubview(infoListTable) + + NSLayoutConstraint.activate([ + infoListTable.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + infoListTable.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + infoListTable.topAnchor.constraint(equalTo: topMenuView.bottomAnchor, constant: spacing), + infoListTable.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) + ]) + } + + func configure(withTitle sceneTitle: String, preSelectedInfos: [Info], tableDatasource: CommonInfoTableDatasource, isMultiselectionAllowed: Bool, endpoint: EndPoint) { + self.sceneTitle = sceneTitle + self.selectedInfo = preSelectedInfos + self.tableDatasource = tableDatasource + self.endpoint = endpoint + + DispatchQueue.main.async { + self.infoListTable.allowsMultipleSelection = isMultiselectionAllowed + } + + setUpTableViewSupporter() + setUpNetworkManager() + } + + private func setUpTableViewSupporter() { + self.tableDelegate = CellSelectionTableDelegate() + tableDelegate?.setCellSelectionHandler(updateSelection) + infoListTable.delegate = tableDelegate + } + + private func updateSelection(index: Int, selectionStatus: CellSelection) { + guard let tableDatasource = tableDatasource, + let targetInfo = tableDatasource.info(for: index) else { return } + + switch selectionStatus { + case .selected: + selectedInfo.append(targetInfo) + case .deSelected: + var targetIndex: Int? + selectedInfo.enumerated().forEach { (index, info) in + if info.identifier() == targetInfo.identifier() { + targetIndex = index + } + } + guard let targetIndex = targetIndex else { return } + selectedInfo.remove(at: targetIndex) + } + changeSaveButtonEnableStatus() + } + + private func setUpNetworkManager() { + let loginInfo = LoginInfo.shared + guard let jwt = loginInfo.jwt else { return } + let headers = [Header.authorization.key(): jwt.description] + let networkManager = NetworkManager(baseAddress: EndPoint.baseAddress, headers: headers) + self.networkManager = networkManager + } + + func setSaveOperation(_ operation: @escaping ([Info]) -> Void) { + self.saveOperation = operation + } + + private func changeSaveButtonEnableStatus() { + DispatchQueue.main.async { + self.saveButton.isEnabled = !self.selectedInfo.isEmpty + } + } + + private func reloadTableView() { + DispatchQueue.main.async { + self.infoListTable.reloadData() + self.setUpCurrentLabelInfo() + } + } + + private func setUpCurrentLabelInfo() { + guard let tableDatasource = tableDatasource else { return } + let selectedIndexs = selectedInfo.compactMap{ tableDatasource.index(for: $0) } + selectedIndexs.forEach { selectedIndex in + let indexPath = IndexPath(row: selectedIndex, section: 0) + + DispatchQueue.main.async { + self.infoListTable.selectRow(at: indexPath, animated: false, scrollPosition: .top) + } + } + } + + @objc private func saveButtonTouched(_ sender: UIButton) { + guard let saveOperation = saveOperation else { return } + saveOperation(selectedInfo) + dismiss(animated: true, completion: nil) + } + + @objc private func cancelButtonTouched(_ sender: UIButton) { + dismiss(animated: true, completion: nil) + } +} + +extension AdditionalInfoViewController { + func loadData() { + guard let networkManager = networkManager, + let endpoint = endpoint, + let tableDatasource = tableDatasource else { return } + + networkManager.get(endpoint: endpoint.path(), queryParameters: nil) { [weak self] (result: Result, NetworkError>) in + switch result { + case .success(let result): + guard let infos = result.data else { return } + tableDatasource.update(with: infos) + self?.reloadTableView() + case .failure(let error): + print("\(error)") + //self?.presentAlert(with: error.description) + } + } + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/CellSelectionTableDelegate.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/CellSelectionTableDelegate.swift new file mode 100644 index 000000000..b1b419aa6 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/CellSelectionTableDelegate.swift @@ -0,0 +1,32 @@ +// +// CellSelectionTableDelegate.swift +// issue-tracker +// +// Created by Song on 2021/06/23. +// + +import UIKit + +final class CellSelectionTableDelegate: NSObject, AdditionalInfoTableDelegate { + + private var cellSelectionHandler: CellSelectionHandler? + + func setCellSelectionHandler(_ handler: @escaping CellSelectionHandler) { + self.cellSelectionHandler = handler + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + reportCellSelection(index: indexPath.row, + selectionStatus: CellSelection.selected) + } + + func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { + reportCellSelection(index: indexPath.row, + selectionStatus: CellSelection.deSelected) + } + + private func reportCellSelection(index: Int, selectionStatus: CellSelection) { + guard let cellSelectionHandler = cellSelectionHandler else { return } + cellSelectionHandler(index, selectionStatus) + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/CommonInfoTableDatasource.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/CommonInfoTableDatasource.swift new file mode 100644 index 000000000..aa0aa243b --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/CommonInfoTableDatasource.swift @@ -0,0 +1,50 @@ +// +// CommonSimpleInfoTableDatasource.swift +// issue-tracker +// +// Created by Song on 2021/06/23. +// + +import UIKit + +final class CommonInfoTableDatasource: NSObject, AdditionalInfoTableDatasource { + + private var infos = [Info]() + private var cellUpdator: CellUpdator? + + func update(with infos: [Info]) { + self.infos = infos + } + + func info(for index: Int) -> Info? { + guard infos.count > index else { return nil } + return infos[index] + } + + func index(for targetInfo: Info) -> Int? { + var targetIndex: Int? + infos.enumerated().forEach { (index, info) in + if info.identifier() == targetInfo.identifier() { + targetIndex = index + } + } + return targetIndex + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return infos.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cellID = InfoCell.reuseID + let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as? InfoCell ?? InfoCell() + guard let cellInfo = info(for: indexPath.row), + let cellUpdator = cellUpdator else { return cell } + return cellUpdator(cell, cellInfo) + } + + func setCellUpdator(_ updator: @escaping (InfoCell, Info) -> InfoCell) { + self.cellUpdator = updator + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/IssueEditViewController.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/IssueEditViewController.swift new file mode 100644 index 000000000..42aecdad8 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/IssueEditViewController.swift @@ -0,0 +1,390 @@ +// +// IssueControlViewController.swift +// issue-tracker +// +// Created by Song on 2021/06/21. +// + +import UIKit +import Down + +final class IssueEditViewController: UIViewController { + + private lazy var cancelButton: ImageBarButton = { + let button = ImageBarButton() + button.configure(with: "chevron.backward", "취소") + button.moveImageToLeft() + button.addTarget(self, action: #selector(cancelButtonTouched), for: .touchUpInside) + return button + }() + + private lazy var saveButton: ImageBarButton = { + let button = ImageBarButton() + button.configure(with: "plus", "저장") + button.addTarget(self, action: #selector(saveButtonTouched), for: .touchUpInside) + changeSaveButtonEnableStatus(baseOn: titleTextField) + return button + }() + + private enum Segment: Int, CaseIterable { + case markdown = 0 + case preview = 1 + + var title: String { + switch self { + case .markdown: + return "마크다운" + case .preview: + return "미리보기" + } + } + } + + private lazy var markdownSegmentedControl: UISegmentedControl = { + let segmentedControl = UISegmentedControl() + let segmentWidth = view.frame.width * 0.25 + + Segment.allCases.forEach { segment in + let segmentIndex = segment.rawValue + segmentedControl.insertSegment(withTitle: segment.title, at: segmentIndex, animated: true) + segmentedControl.setWidth(segmentWidth, forSegmentAt: segmentIndex) + } + + let heavyFontStyle = UIFont.systemFont(ofSize: 14, weight: .heavy) + let fontKey = NSAttributedString.Key.font + segmentedControl.setTitleTextAttributes([fontKey: heavyFontStyle], for: .normal) + segmentedControl.setTitleTextAttributes([fontKey: heavyFontStyle], for: .selected) + + segmentedControl.addTarget(self, action: #selector(markdownSegmentChanged), for: .valueChanged) + segmentedControl.selectedSegmentIndex = Segment.markdown.rawValue + return segmentedControl + }() + + private lazy var titleInputView: UIView = { + let container = UIView() + container.translatesAutoresizingMaskIntoConstraints = false + + let titleLabel = UILabel() + titleLabel.text = "제목" + titleLabel.translatesAutoresizingMaskIntoConstraints = false + + container.addSubview(titleLabel) + + NSLayoutConstraint.activate([ + titleLabel.leadingAnchor.constraint(equalTo: container.safeAreaLayoutGuide.leadingAnchor, constant: spacing), + titleLabel.centerYAnchor.constraint(equalTo: container.safeAreaLayoutGuide.centerYAnchor) + ]) + + container.addSubview(titleTextField) + + NSLayoutConstraint.activate([ + titleTextField.leadingAnchor.constraint(equalTo: container.safeAreaLayoutGuide.leadingAnchor, constant: spacing * 3), + titleTextField.trailingAnchor.constraint(equalTo: container.safeAreaLayoutGuide.trailingAnchor, constant: -spacing), + titleTextField.centerYAnchor.constraint(equalTo: container.safeAreaLayoutGuide.centerYAnchor) + ]) + + let line = CALayer() + let borderWidth: CGFloat = 1 + let size = CGSize(width: view.frame.width - spacing, height: borderWidth) + let origin = CGPoint(x: spacing, y: lineHeight - borderWidth) + line.frame = CGRect(origin: origin, size: size) + line.backgroundColor = Colors.border.cgColor + container.layer.addSublayer(line) + return container + }() + + private lazy var titleTextField: UITextField = { + let textField = UITextField() + textField.placeholder = "(필수 입력)" + textField.translatesAutoresizingMaskIntoConstraints = false + textField.delegate = self + return textField + }() + + private lazy var markdownStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [bodyTextView, markdownView]) + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private lazy var bodyTextView: UITextView = { + let textView = UITextView() + textView.text = bodyPlaceholder + textView.font = .systemFont(ofSize: 17) + textView.textColor = .lightGray + textView.dataDetectorTypes = UIDataDetectorTypes.all + textView.translatesAutoresizingMaskIntoConstraints = false + textView.delegate = self + return textView + }() + + private lazy var markdownView: DownView = { + let downView = try! DownView(frame: .zero, markdownString: "") + downView.pageZoom = 1.5 + downView.isHidden = true + return downView + }() + + private lazy var additionalInfoView: MultipleLineInputStackView = { + let viewWidth = view.frame.width + let stackViewFrame = CGRect(x: 0, y: 0, width: viewWidth, height: lineHeight * 3) + let stackView = MultipleLineInputStackView(frame: stackViewFrame) + + let labelInfoItem = InputLineItem(category: "레이블", inputView: labelInfoControl) + let milestoneInfoItem = InputLineItem(category: "마일스톤", inputView: milestoneInfoControl) + let assigneeInfoItem = InputLineItem(category: "담당자", inputView: assigneeInfoControl) + stackView.configure(with: [labelInfoItem, milestoneInfoItem, assigneeInfoItem]) + + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private lazy var labelInfoControl: IssueInfoControl = { + let infoControl = IssueInfoControl() + infoControl.addTarget(self, action: #selector(labelInfoTouched), for: .touchUpInside) + return infoControl + }() + + private lazy var milestoneInfoControl: IssueInfoControl = { + let infoControl = IssueInfoControl() + infoControl.addTarget(self, action: #selector(milestoneInfoTouched), for: .touchUpInside) + return infoControl + }() + + private lazy var assigneeInfoControl: IssueInfoControl = { + let infoControl = IssueInfoControl() + infoControl.addTarget(self, action: #selector(assigneeInfoTouched), for: .touchUpInside) + return infoControl + }() + + private let bodyPlaceholder = "이곳에 내용을 입력하세요" + + private lazy var lineHeight: CGFloat = { + return view.frame.height * 0.05 + }() + + private lazy var spacing: CGFloat = { + return lineHeight * 0.5 + }() + + private var currentIssue: Issue? + private var selectedLabels: [Label]? + private var selectedMilestone: [MileStone]? + private var selectedAssignees: [User]? + private var saveOperation: ((NewIssue) -> Void)? + + override func viewDidLoad() { + super.viewDidLoad() + configureViews() + } + + private func configureViews() { + view.backgroundColor = .white + navigationController?.navigationBar.prefersLargeTitles = false + navigationItem.rightBarButtonItem?.isEnabled = saveButton.isEnabled + + addNavigationItems() + addTitleInputView() + addAdditionalInfoView() + addBodyTextView() + } + + private func addNavigationItems() { + navigationItem.leftBarButtonItem = UIBarButtonItem(customView: cancelButton) + navigationItem.rightBarButtonItem = UIBarButtonItem(customView: saveButton) + navigationItem.titleView = markdownSegmentedControl + } + + private func addTitleInputView() { + view.addSubview(titleInputView) + + NSLayoutConstraint.activate([ + titleInputView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: spacing * 0.7), + titleInputView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + titleInputView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + titleInputView.heightAnchor.constraint(equalToConstant: lineHeight) + ]) + } + + private func addAdditionalInfoView() { + view.addSubview(additionalInfoView) + + NSLayoutConstraint.activate([ + additionalInfoView.heightAnchor.constraint(equalToConstant: lineHeight * 3), + additionalInfoView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + additionalInfoView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + additionalInfoView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) + ]) + } + + private func addBodyTextView() { + view.addSubview(markdownStackView) + + NSLayoutConstraint.activate([ + markdownStackView.topAnchor.constraint(equalTo: titleInputView.bottomAnchor, constant: spacing * 0.7), + markdownStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: spacing * 0.8), + markdownStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -spacing * 0.8), + markdownStackView.bottomAnchor.constraint(equalTo: additionalInfoView.topAnchor) + ]) + } + + func setSaveOperation(_ operation: @escaping (NewIssue) -> Void) { + self.saveOperation = operation + } + + private func changeSaveButtonEnableStatus(to status: Bool) { + saveButton.isEnabled = status + } + + @objc private func cancelButtonTouched(_ sender: UIButton) { + popCurrentViewController() + } + + @objc private func saveButtonTouched(_ sender: UIButton) { + guard let saveOperation = saveOperation, + let title = titleTextField.text else { return } + let newIssue = NewIssue(title: title, + comment: bodyTextView.text, + assigneeIds: selectedAssignees?.map{ $0.id }, + labelIds: selectedLabels?.map{ $0.id }, + milestoneId: selectedMilestone?.first?.id) + saveOperation(newIssue) + popCurrentViewController() + } + + private func popCurrentViewController() { + navigationController?.popViewController(animated: true) + navigationController?.navigationBar.prefersLargeTitles = true + } + + @objc private func markdownSegmentChanged(_ sender: UISegmentedControl) { + let currentIndex = sender.selectedSegmentIndex + + switch currentIndex { + case Segment.markdown.rawValue: + previewToMarkDown() + case Segment.preview.rawValue: + markdownToPreview() + default: + assert(false) + } + } + + private func previewToMarkDown() { + markdownView.isHidden = true + bodyTextView.isHidden = false + } + + private func markdownToPreview() { + guard let rawText = bodyTextView.text else { return } + try? markdownView.update(markdownString: rawText) + markdownView.isHidden = false + bodyTextView.isHidden = true + } + + @objc private func labelInfoTouched(_ sender: UIButton) { + let tableDatasource = CommonInfoTableDatasource() + tableDatasource.setCellUpdator(LabelInfoTableViewCell.update) + + let labelInfoViewController = AdditionalInfoViewController() + labelInfoViewController.configure(withTitle: "레이블 선택", + preSelectedInfos: selectedLabels ?? [], + tableDatasource: tableDatasource, + isMultiselectionAllowed: true, + endpoint: EndPoint.label) + labelInfoViewController.setSaveOperation(updateLabelSelection) + + present(labelInfoViewController) + } + + private func present(_ viewController: UIViewController) { + DispatchQueue.main.async { + self.present(viewController, animated: true, completion: nil) + } + } + + private func updateLabelSelection(labels: [Label]) { + guard let firstLabel = labels.first else { return } + self.selectedLabels = labels + let extraCount = labels.count - 1 + let tail = extraCount > 0 ? " 외 \(extraCount)개" : "" + let labelInfo = "\(firstLabel.title)" + tail + labelInfoControl.changeInfoLabelText(to: labelInfo) + } + + @objc private func milestoneInfoTouched(_ sender: UIButton) { + let tableDatasource = CommonInfoTableDatasource() + tableDatasource.setCellUpdator(MilestoneInfoTableViewCell.update) + + let milestoneViewController = AdditionalInfoViewController() + milestoneViewController.configure(withTitle: "마일스톤 선택", + preSelectedInfos: selectedMilestone ?? [], + tableDatasource: tableDatasource, + isMultiselectionAllowed: false, + endpoint: EndPoint.milestone) + milestoneViewController.setSaveOperation(updateMilestoneSelection) + + present(milestoneViewController) + } + + private func updateMilestoneSelection(milestones: [MileStone]) { + guard let firstMilestone = milestones.first else { return } + self.selectedMilestone = milestones + milestoneInfoControl.changeInfoLabelText(to: firstMilestone.title) + } + + @objc private func assigneeInfoTouched(_ sender: UIButton) { + let tableDatasource = CommonInfoTableDatasource() + tableDatasource.setCellUpdator(AssigneeInfoTableViewCell.update) + + let assigneeViewController = AdditionalInfoViewController() + assigneeViewController.configure(withTitle: "담당자 선택", + preSelectedInfos: selectedAssignees ?? [], + tableDatasource: tableDatasource, + isMultiselectionAllowed: true, + endpoint: EndPoint.user) + assigneeViewController.setSaveOperation(updateAssigneeSelection) + + present(assigneeViewController) + } + + private func updateAssigneeSelection(assignees: [User]) { + guard let firstAssignee = assignees.first else { return } + self.selectedAssignees = assignees + let extraCount = assignees.count - 1 + let tail = extraCount > 0 ? " 외 \(extraCount)명" : "" + let assigneeInfo = "\(firstAssignee.name)" + tail + assigneeInfoControl.changeInfoLabelText(to: assigneeInfo) + } +} + +extension IssueEditViewController: UITextFieldDelegate { + func textFieldDidChangeSelection(_ textField: UITextField) { + changeSaveButtonEnableStatus(baseOn: textField) + } + + private func changeSaveButtonEnableStatus(baseOn textField: UITextField) { + DispatchQueue.main.async { + self.saveButton.isEnabled = !textField.isEmpty() + } + } +} + +extension IssueEditViewController: UITextViewDelegate { + func textViewDidBeginEditing(_ textView: UITextView) { + if textView.textColor == .lightGray { + textView.textColor = .black + textView.text = nil + } + } + + func textViewDidEndEditing(_ textView: UITextView) { + if textView.text.isEmpty { + textView.textColor = .lightGray + textView.text = bodyPlaceholder + changeSaveButtonEnableStatus(to: false) + } + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/IssueViewController.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/IssueViewController.swift new file mode 100644 index 000000000..9294d48cb --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/IssueViewController.swift @@ -0,0 +1,182 @@ +// +// IssueViewController.swift +// issue-tracker +// +// Created by Song on 2021/06/09. +// + +import UIKit + +final class IssueViewController: UIViewController { + + private lazy var issueTableView: UITableView = { + let tableView = UITableView() + let cellID = IssueTableViewCell.reuseID + tableView.register(IssueTableViewCell.self, forCellReuseIdentifier: cellID) + tableView.backgroundColor = Colors.background + tableView.translatesAutoresizingMaskIntoConstraints = false + return tableView + }() + + private lazy var addNewIssueButton: UIButton = { + let button = UIButton() + button.backgroundColor = Colors.mainGrape + button.addTarget(self, action: #selector(addNewIssue), for: .touchUpInside) + button.layer.masksToBounds = true + button.layer.cornerRadius = 32 + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + private lazy var plusImageView: UIImageView = { + let image = UIImage(systemName: "plus") + let resizeImg = image?.resizedImage(size: CGSize(width: 32, height: 34))?.withRenderingMode(.alwaysTemplate) + let imageView = UIImageView(image: resizeImg) + imageView.tintColor = UIColor.white + imageView.backgroundColor = Colors.mainGrape + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private var networkManager: NetworkManagerOperations? + private var issueTableDatasource: IssueTableViewDataSource? + private var issueTableDelegate: IssueTableViewDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + title = "이슈 선택" + view.backgroundColor = UIColor.white + + addTableView() + addButton() + setTableViewSupporters() + setNetworkManager() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + loadIssues() + } + + private func addTableView() { + view.addSubview(issueTableView) + + NSLayoutConstraint.activate([ + issueTableView.topAnchor.constraint(equalTo: view.topAnchor), + issueTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + issueTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + issueTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } + + private func addButton() { + view.addSubview(addNewIssueButton) + + NSLayoutConstraint.activate([ + addNewIssueButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + addNewIssueButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -90), + addNewIssueButton.widthAnchor.constraint(equalToConstant: 64), + addNewIssueButton.heightAnchor.constraint(equalToConstant: 64) + ]) + + + addNewIssueButton.addSubview(plusImageView) + + NSLayoutConstraint.activate([ + plusImageView.centerXAnchor.constraint(equalTo: addNewIssueButton.centerXAnchor), + plusImageView.centerYAnchor.constraint(equalTo: addNewIssueButton.centerYAnchor) + ]) + } + + private func setTableViewSupporters() { + issueTableDatasource = IssueTableViewDataSource() + issueTableDelegate = IssueTableViewDelegate(cellActionHandler: swipeActionHandler, cellHeight: 198) + + issueTableView.delegate = issueTableDelegate + issueTableView.dataSource = issueTableDatasource + } + + + private func setNetworkManager() { + let loginInfo = LoginInfo.shared + guard let jwt = loginInfo.jwt else { return } + let headers = [Header.authorization.key(): jwt.description] + networkManager = NetworkManager(baseAddress: EndPoint.baseAddress, headers: headers) + } + + private func reloadTableView() { + DispatchQueue.main.async { + self.issueTableView.reloadData() + } + } + + private func swipeActionHandler(_ index: Int, _ action: CellAction) { + guard let targetIssue = issueTableDatasource?.issues[index] else { return } + + switch action { + case .delete: + deleteIssue(for: targetIssue.issueNumber) + case .close: + print("close 되어랏") + default: + assert(false) + } + } + + private func presentAlert(with errorMessage: String) { + DispatchQueue.main.async { + let alert = AlertFactory.create(body: errorMessage) + self.present(alert, animated: true, completion: nil) + } + } + + @objc func addNewIssue(_ sender: UIButton) { + let nextViewController = IssueEditViewController() + nextViewController.setSaveOperation(postIssue) + navigationController?.pushViewController(nextViewController, animated: false) + } +} + +//MARK: - Network Methods +extension IssueViewController { + private func loadIssues() { + let issueListEndpoint = EndPoint.issue.path() + networkManager?.get(endpoint: issueListEndpoint, queryParameters: nil, + completion: { [weak self] (result: Result) in + switch result { + case .success(let result): + guard let issues = result.data else { return } + self?.issueTableDatasource?.update(issues: issues) + self?.issueTableDelegate?.update(issues: issues) + self?.reloadTableView() + case .failure(let error): + self?.presentAlert(with: error.description) + } + }) + } + + private func deleteIssue(for id: Int) { + let deleteIssueEndpoint = EndPoint.issue.path(with: id) + networkManager?.delete(endpoint: deleteIssueEndpoint, queryParameters: nil, completion: { [weak self] (result: Result) in + switch result { + case .success(_): + self?.loadIssues() + case .failure(let error): + self?.presentAlert(with: error.description) + } + }) + } + + private func postIssue(_ newIssue: NewIssue) { + let postIssueEndpoint = EndPoint.issue.path() + networkManager?.post(endpoint: postIssueEndpoint, requestBody: newIssue, completion: { [weak self] (result: Result) in + switch result { + case .success(_): + self?.loadIssues() + case .failure(let error): + self?.presentAlert(with: error.description) + } + }) + } +} + diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/Protocol/AdditionalInfoTableDatasource.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/Protocol/AdditionalInfoTableDatasource.swift new file mode 100644 index 000000000..c7446a118 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/Protocol/AdditionalInfoTableDatasource.swift @@ -0,0 +1,18 @@ +// +// SimpleInfoTableDatasource.swift +// issue-tracker +// +// Created by Song on 2021/06/23. +// + +import UIKit + +protocol AdditionalInfoTableDatasource: UITableViewDataSource { + associatedtype Info + associatedtype InfoCell + typealias CellUpdator = (InfoCell, Info) -> InfoCell + func update(with infos: [Info]) + func info(for index: Int) -> Info? + func index(for info: Info) -> Int? + func setCellUpdator(_ updator: @escaping CellUpdator) +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/Protocol/AdditionalInfoTableDelegate.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/Protocol/AdditionalInfoTableDelegate.swift new file mode 100644 index 000000000..bc41e523a --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueEditing/Protocol/AdditionalInfoTableDelegate.swift @@ -0,0 +1,18 @@ +// +// SimpleInfoTableDelegate.swift +// issue-tracker +// +// Created by Song on 2021/06/23. +// + +import UIKit + +enum CellSelection { + case selected + case deSelected +} + +protocol AdditionalInfoTableDelegate: UITableViewDelegate { + typealias CellSelectionHandler = ((Int, CellSelection) -> Void) + func setCellSelectionHandler(_ handler: @escaping CellSelectionHandler) +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueTableViewDataSource.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueTableViewDataSource.swift new file mode 100644 index 000000000..ab3ae642a --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueTableViewDataSource.swift @@ -0,0 +1,30 @@ +// +// IssueTableViewDataSource.swift +// issue-tracker +// +// Created by jinseo park on 6/21/21. +// + +import Foundation +import UIKit + +class IssueTableViewDataSource: NSObject, UITableViewDataSource { + + private(set) var issues = [Issue]() + + func update(issues: [Issue]) { + self.issues = issues + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return issues.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cellID = IssueTableViewCell.reuseID + let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as? IssueTableViewCell ?? IssueTableViewCell() + let issue = issues[indexPath.row] + cell.configure(title: issue.title ?? "", mileStoneName: issue.milestone?.title ?? "", labels: issue.labels ?? []) + return cell + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueTableViewDelegate.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueTableViewDelegate.swift new file mode 100644 index 000000000..d9edc9b5e --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueTableViewDelegate.swift @@ -0,0 +1,51 @@ +// +// IssueTableViewDelegate.swift +// issue-tracker +// +// Created by jinseo park on 6/21/21. +// + +import UIKit + +class IssueTableViewDelegate: NSObject, UITableViewDelegate { + + typealias CellActionHandler = (Int, CellAction) -> Void + private var cellActionHandler: CellActionHandler + private var cellHeight: CGFloat + private(set) var issues = [Issue]() + + func update(issues: [Issue]) { + self.issues = issues + } + + init(cellActionHandler: @escaping CellActionHandler, cellHeight: CGFloat) { + self.cellActionHandler = cellActionHandler + self.cellHeight = cellHeight + } + + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return self.cellHeight + } + + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + let deleteAction = UIContextualAction(style: .destructive, + title: CellAction.delete.buttonTitle()) { [weak self] _, _, _ in + self?.cellActionHandler(indexPath.row, .delete) + } + deleteAction.image = UIImage(systemName: "trash") + + let closeAction = UIContextualAction(style: .normal, + title: CellAction.close.buttonTitle()) { [weak self] _, _, _ in + self?.cellActionHandler(indexPath.row, .close) + } + closeAction.image = UIImage(systemName: "archivebox") + + return UISwipeActionsConfiguration(actions: [closeAction, deleteAction]) + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + print("clicked: ", issues[indexPath.row]) + //navigation + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueViewController.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueViewController.swift new file mode 100644 index 000000000..b27e2d7f0 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Issue/Controller/IssueViewController.swift @@ -0,0 +1,168 @@ +// +// IssueViewController.swift +// issue-tracker +// +// Created by Song on 2021/06/09. +// + +import UIKit + +class IssueViewController: UIViewController { + + private lazy var issueTableView: UITableView = { + let tableView = UITableView() + let cellID = IssueTableViewCell.reuseID + tableView.register(IssueTableViewCell.self, forCellReuseIdentifier: cellID) + tableView.backgroundColor = Colors.background + tableView.translatesAutoresizingMaskIntoConstraints = false + return tableView + }() + + private lazy var addNewIssueButton: UIButton = { + let button = UIButton() + button.backgroundColor = Colors.mainGrape + button.addTarget(self, action: #selector(addNewIssue), for: .touchUpInside) + button.layer.masksToBounds = true + button.layer.cornerRadius = 32 + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + private lazy var plusImageView: UIImageView = { + let image = UIImage(systemName: "plus") + let resizeImg = image?.resizedImage(size: CGSize(width: 32, height: 34))?.withRenderingMode(.alwaysTemplate) + let imageView = UIImageView(image: resizeImg) + imageView.tintColor = UIColor.white + imageView.backgroundColor = Colors.mainGrape + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private var networkManager: NetworkManagerOperations? + private var issueTableDatasource: IssueTableViewDataSource? + private var issueTableDelegate: IssueTableViewDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + title = "이슈 선택" + view.backgroundColor = UIColor.white + + addTableView() + addButton() + setTableViewSupporters() + setNetworkManager() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + loadIssues() + } + + private func addTableView() { + view.addSubview(issueTableView) + + NSLayoutConstraint.activate([ + issueTableView.topAnchor.constraint(equalTo: view.topAnchor), + issueTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + issueTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + issueTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } + + private func addButton() { + view.addSubview(addNewIssueButton) + + NSLayoutConstraint.activate([ + addNewIssueButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + addNewIssueButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -90), + addNewIssueButton.widthAnchor.constraint(equalToConstant: 64), + addNewIssueButton.heightAnchor.constraint(equalToConstant: 64) + ]) + + + addNewIssueButton.addSubview(plusImageView) + + NSLayoutConstraint.activate([ + plusImageView.centerXAnchor.constraint(equalTo: addNewIssueButton.centerXAnchor), + plusImageView.centerYAnchor.constraint(equalTo: addNewIssueButton.centerYAnchor) + ]) + } + + private func setTableViewSupporters() { + issueTableDatasource = IssueTableViewDataSource() + issueTableDelegate = IssueTableViewDelegate(cellActionHandler: swipeActionHandler, cellHeight: 198) + + issueTableView.delegate = issueTableDelegate + issueTableView.dataSource = issueTableDatasource + } + + + private func setNetworkManager() { + let loginInfo = LoginInfo.shared + guard let jwt = loginInfo.jwt else { return } + let headers = [Header.authorization.key(): jwt.description] + networkManager = NetworkManager(baseAddress: EndPoint.baseAddress, headers: headers) + } + + private func reloadTableView() { + DispatchQueue.main.async { + self.issueTableView.reloadData() + } + } + + private func swipeActionHandler(_ index: Int, _ action: CellAction) { + guard let targetIssue = issueTableDatasource?.issues[index] else { return } + + switch action { + case .delete: + deleteIssue(for: targetIssue.issueNumber) + case .close: + print("close 되어랏") + default: + assert(false) + } + } + + private func presentAlert(with errorMessage: String) { + DispatchQueue.main.async { + let alert = AlertFactory.create(body: errorMessage) + self.present(alert, animated: true, completion: nil) + } + } + + @objc func addNewIssue(_ sender: UIButton) { + print("야호~") + } +} + +//MARK: - Network Methods +extension IssueViewController { + private func loadIssues() { + let issueListEndpoint = EndPoint.issue.path() + networkManager?.get(endpoint: issueListEndpoint, queryParameters: nil, + completion: { [weak self] (result: Result) in + switch result { + case .success(let result): + guard let issues = result.data else { return } + self?.issueTableDatasource?.update(issues: issues) + self?.issueTableDelegate?.update(issues: issues) + self?.reloadTableView() + case .failure(let error): + self?.presentAlert(with: error.description) + } + }) + } + + private func deleteIssue(for id: Int) { + let deleteIssueEndpoint = EndPoint.issue.path(with: id) + networkManager?.delete(endpoint: deleteIssueEndpoint, queryParameters: nil, completion: { [weak self] (result: Result) in + switch result { + case .success(_): + self?.loadIssues() + case .failure(let error): + self?.presentAlert(with: error.description) + } + }) + } +} + diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/IssueViewController.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/IssueViewController.swift deleted file mode 100644 index 803fb619c..000000000 --- a/iOS/issue-tracker/issue-tracker/Main/Issue/IssueViewController.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// IssueViewController.swift -// issue-tracker -// -// Created by Song on 2021/06/09. -// - -import UIKit - -class IssueViewController: UIViewController { - - private var loginInfo: LoginInfo? - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = UIColor.red - title = "이슈 선택" - } - -} - -extension IssueViewController: LoginInfoContainer { - func setup(loginInfo: LoginInfo) { - self.loginInfo = loginInfo - } -} diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/Model/Issue.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/Model/Issue.swift new file mode 100644 index 000000000..dcee5c6fa --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Issue/Model/Issue.swift @@ -0,0 +1,64 @@ +// +// Issue.swift +// issue-tracker +// +// Created by jinseo park on 6/22/21. +// + +import Foundation + +struct IssueDTO: Decodable { + let data: [Issue]? + let message: String? + + enum CodingKeys: String, CodingKey { + case data + case message = "msg" + } +} + +struct Author: Decodable { + private(set) var id: Int + private(set) var name: String + private(set) var imageUrl: String + + enum CodingKeys: String, CodingKey { + case id + case name + case imageUrl + } +} + +struct Assignee: Decodable { + private(set) var id: Int + private(set) var name: String + private(set) var imageUrl: String + + enum CodingKeys: String, CodingKey { + case id + case name + case imageUrl + } +} + +struct Issue: Decodable { + private(set) var issueNumber: Int + private(set) var title: String? + private(set) var status: Bool + private(set) var author: Author + private(set) var assignees: [Assignee]? + private(set) var labels: [Label]? + private(set) var milestone: MileStone? + private(set) var createdDate: String + + enum CodingKeys: String, CodingKey { + case issueNumber + case title + case status + case author + case assignees + case labels + case milestone + case createdDate = "created_date" + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/Model/NewIssue.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/Model/NewIssue.swift new file mode 100644 index 000000000..a3fd0c7d3 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Issue/Model/NewIssue.swift @@ -0,0 +1,24 @@ +// +// NewIssue.swift +// issue-tracker +// +// Created by Song on 2021/06/23. +// + +import Foundation + +struct NewIssue: Encodable { + let title: String + let comment: String + let assigneeIds: [Int] + let labelIds: [Int] + let milestoneId: Int? + + init(title: String, comment: String?, assigneeIds: [Int]?, labelIds: [Int]?, milestoneId: Int?) { + self.title = title + self.comment = comment ?? "" + self.assigneeIds = assigneeIds ?? [] + self.labelIds = labelIds ?? [] + self.milestoneId = milestoneId + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/Model/User.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/Model/User.swift new file mode 100644 index 000000000..b09d3351c --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Issue/Model/User.swift @@ -0,0 +1,18 @@ +// +// User.swift +// issue-tracker +// +// Created by Song on 2021/06/23. +// + +import Foundation + +struct User: Decodable, Identifiable { + private(set) var id: Int + private(set) var name: String + private(set) var imageUrl: String + + func identifier() -> Int { + return id + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/View/IssueEditing/AssigneeInfoTableViewCell.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/View/IssueEditing/AssigneeInfoTableViewCell.swift new file mode 100644 index 000000000..5124c13df --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Issue/View/IssueEditing/AssigneeInfoTableViewCell.swift @@ -0,0 +1,82 @@ +// +// SimpleAssigneeTableViewCell.swift +// issue-tracker +// +// Created by Song on 2021/06/23. +// + +import UIKit + +final class AssigneeInfoTableViewCell: UITableViewCell { + + private lazy var titleLabel = UILabel() + + private lazy var profileImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private let spacing: CGFloat = 15 + static let imageManager = ImageLoadManager() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } + + override func awakeFromNib() { + super.awakeFromNib() + configure() + } + + private func configure() { + selectionStyle = .none + addImageView() + addLabelView() + } + + private func addImageView() { + addSubview(profileImageView) + + NSLayoutConstraint.activate([ + profileImageView.centerYAnchor.constraint(equalTo: centerYAnchor), + profileImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: spacing), + profileImageView.heightAnchor.constraint(equalToConstant: frame.height * 0.8), + profileImageView.widthAnchor.constraint(equalToConstant: frame.height * 0.8) + ]) + } + + private func addLabelView() { + titleLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(titleLabel) + + NSLayoutConstraint.activate([ + titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + titleLabel.leadingAnchor.constraint(equalTo: profileImageView.trailingAnchor, constant: spacing), + titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -spacing * 2) + ]) + } + + static func update(cell: AssigneeInfoTableViewCell, with user: User) -> AssigneeInfoTableViewCell { + let title = user.name + cell.titleLabel.text = title + + let imageUrl = user.imageUrl + imageManager.load(from: imageUrl) { cachePath in + cell.profileImageView.image = UIImage(contentsOfFile: cachePath) + } + return cell + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + accessoryType = selected ? .checkmark : .none + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/View/IssueEditing/IssueInfoView.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/View/IssueEditing/IssueInfoView.swift new file mode 100644 index 000000000..e6b3c32c2 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Issue/View/IssueEditing/IssueInfoView.swift @@ -0,0 +1,70 @@ +// +// IssueInfoView.swift +// issue-tracker +// +// Created by Song on 2021/06/22. +// + +import UIKit + +final class IssueInfoControl: UIControl { + + private lazy var infoLabel: UILabel = { + let label = UILabel() + label.textColor = .lightGray + label.textAlignment = .right + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var infoSetButton: UIButton = { + let button = UIButton() + let buttonImage = UIImage(systemName: "chevron.right") + button.tintColor = .lightGray + button.setTitle(nil, for: .normal) + button.setImage(buttonImage, for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + private let spacing: CGFloat = 15 + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } + + private func configure() { + addInfoSetButton() + addInfoLabel() + } + + private func addInfoSetButton() { + addSubview(infoSetButton) + + NSLayoutConstraint.activate([ + infoSetButton.widthAnchor.constraint(equalToConstant: 20), + infoSetButton.trailingAnchor.constraint(equalTo: trailingAnchor), + infoSetButton.centerYAnchor.constraint(equalTo: centerYAnchor) + ]) + } + + private func addInfoLabel() { + addSubview(infoLabel) + + NSLayoutConstraint.activate([ + infoLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + infoLabel.trailingAnchor.constraint(equalTo: infoSetButton.leadingAnchor, constant: -spacing), + infoLabel.centerYAnchor.constraint(equalTo: centerYAnchor) + ]) + } + + func changeInfoLabelText(to text: String) { + infoLabel.text = text + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/View/IssueEditing/LabelInfoTableViewCell.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/View/IssueEditing/LabelInfoTableViewCell.swift new file mode 100644 index 000000000..2ac2b5031 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Issue/View/IssueEditing/LabelInfoTableViewCell.swift @@ -0,0 +1,55 @@ +// +// SimpleLabelTableViewCell.swift +// issue-tracker +// +// Created by Song on 2021/06/23. +// + +import UIKit + +final class LabelInfoTableViewCell: UITableViewCell { + + private lazy var labelView = LabelView() + private let spacing: CGFloat = 15 + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } + + override func awakeFromNib() { + super.awakeFromNib() + configure() + } + + private func configure() { + selectionStyle = .none + addLabelView() + } + + private func addLabelView() { + addSubview(labelView) + + NSLayoutConstraint.activate([ + labelView.centerYAnchor.constraint(equalTo: centerYAnchor), + labelView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: spacing) + ]) + } + + static func update(cell: LabelInfoTableViewCell, with label: Label) -> LabelInfoTableViewCell { + let hexColorCode = HexColorCode(from: label.hexColorCode) + let title = label.title + cell.labelView.configure(with: hexColorCode, title) + return cell + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + accessoryType = selected ? .checkmark : .none + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/View/IssueEditing/MilestoneInfoTableViewCell.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/View/IssueEditing/MilestoneInfoTableViewCell.swift new file mode 100644 index 000000000..d1ef92e2f --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Issue/View/IssueEditing/MilestoneInfoTableViewCell.swift @@ -0,0 +1,56 @@ +// +// SimpleMilestoneTableViewCell.swift +// issue-tracker +// +// Created by Song on 2021/06/23. +// + +import UIKit + +final class MilestoneInfoTableViewCell: UITableViewCell { + + private lazy var titleLabel = UILabel() + private let spacing: CGFloat = 15 + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } + + override func awakeFromNib() { + super.awakeFromNib() + configure() + } + + private func configure() { + selectionStyle = .none + addLabelView() + } + + private func addLabelView() { + titleLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(titleLabel) + + NSLayoutConstraint.activate([ + titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: spacing), + titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -spacing * 2) + ]) + } + + static func update(cell: MilestoneInfoTableViewCell, with milestone: MileStone) -> MilestoneInfoTableViewCell { + let title = milestone.title + cell.titleLabel.text = title + return cell + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + accessoryType = selected ? .checkmark : .none + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Issue/View/IssueTableViewCell.swift b/iOS/issue-tracker/issue-tracker/Main/Issue/View/IssueTableViewCell.swift new file mode 100644 index 000000000..94478eaa6 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Issue/View/IssueTableViewCell.swift @@ -0,0 +1,133 @@ +// +// IssueTableViewCell.swift +// issue-tracker +// +// Created by jinseo park on 6/21/21. +// + +import UIKit + +class IssueTableViewCell: UITableViewCell { + + private let spacing: CGFloat = 16 + private lazy var issueStackView: UIStackView = { + let superStackView = UIStackView() + superStackView.axis = .vertical + superStackView.distribution = .fillProportionally + superStackView.translatesAutoresizingMaskIntoConstraints = false + superStackView.spacing = 1 + return superStackView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.text = "이슈 제목" + label.font = UIFont.boldSystemFont(ofSize: 22) + return label + }() + + private lazy var labelsStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.distribution = .fillProportionally + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private lazy var mileStoneLabel: UILabel = { + let label = UILabel() + + //추후에 지울 것들. + let attributedString = NSMutableAttributedString(string: "") + let imageAttachment = NSTextAttachment() + let attrs = [NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 13), NSAttributedString.Key.foregroundColor : UIColor.systemGray] + let mileStoneName = NSMutableAttributedString(string:"마일스톤 이름", attributes:attrs) + + imageAttachment.image = UIImage(systemName: "signpost.right") + attributedString.append(NSAttributedString(attachment: imageAttachment)) + attributedString.append(mileStoneName) + label.attributedText = attributedString + + return label + }() + + required init?(coder: NSCoder) { + super.init(coder: coder) + setViews() + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setViews() + } + + private func setViews() { + addIssueStackView() + addIssueTitleView() + addMileStoneLabel() + } + + func addIssueStackView() { + addSubview(issueStackView) + NSLayoutConstraint.activate([ + issueStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + issueStackView.topAnchor.constraint(equalTo: topAnchor, constant: 24), + issueStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -24), + issueStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16) + ]) + } + + func addIssueTitleView() { + issueStackView.addArrangedSubview(titleLabel) + } + + func addMileStoneLabel() { + issueStackView.addArrangedSubview(mileStoneLabel) + } + + func clearLabelStackView() { + labelsStackView.subviews.forEach { $0.removeFromSuperview() } + } + + func addlabelsStackView(_ labels: [Label]) { + issueStackView.addArrangedSubview(labelsStackView) + labelsStackView.spacing = 4 + + NSLayoutConstraint.activate([ + labelsStackView.leadingAnchor.constraint(equalTo: issueStackView.leadingAnchor), + labelsStackView.trailingAnchor.constraint(equalTo: issueStackView.trailingAnchor) + ]) + + labels.forEach { label in + let labelView = LabelView() + let colorText = label.hexColorCode + let hex = HexColorCode(from: colorText) + let titleText = label.title + labelView.configure(with: hex, titleText) + labelView.translatesAutoresizingMaskIntoConstraints = false + labelsStackView.addArrangedSubview(labelView) + } + } + + func configure(title: String, mileStoneName: String, labels: [Label]) { + titleLabel.text = title + mileStoneTitleConfigure(mileStoneName: mileStoneName) + clearLabelStackView() + addlabelsStackView(labels) + } + + private func mileStoneTitleConfigure(mileStoneName: String) { + let mileStoneText = mileStoneName != "" ? mileStoneName :"마일스톤 이름" + let attributedString = NSMutableAttributedString(string: "") + let imageAttachment = NSTextAttachment() + imageAttachment.image = UIImage(systemName: "signpost.right") + + let attrs = [NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 13), NSAttributedString.Key.foregroundColor : Colors.description] + let dateString = NSMutableAttributedString(string:mileStoneText, attributes:attrs) + + attributedString.append(NSAttributedString(attachment: imageAttachment)) + attributedString.append(dateString) + mileStoneLabel.attributedText = attributedString + } + +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Label/Controller/LabelControlViewController.swift b/iOS/issue-tracker/issue-tracker/Main/Label/Controller/LabelControlViewController.swift new file mode 100644 index 000000000..dcdba493b --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Label/Controller/LabelControlViewController.swift @@ -0,0 +1,240 @@ +// +// LabelControlViewController.swift +// issue-tracker +// +// Created by Song on 2021/06/18. +// + +import UIKit + +class LabelControlViewController: UIViewController { + + private lazy var topMenuView: TopMenuView = { + let topMenuView = TopMenuView() + topMenuView.configure(withTitle: sceneTitle, rightButton: saveButton, leftButton: cancelButton) + topMenuView.translatesAutoresizingMaskIntoConstraints = false + return topMenuView + }() + + private lazy var saveButton: ImageBarButton = { + let button = ImageBarButton() + button.configure(with: "", "저장") + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(saveButtonTouched), for: .touchUpInside) + changeSaveButtonEnableStatus(baseOn: titleTextfield) + return button + }() + + private lazy var cancelButton: ImageBarButton = { + let button = ImageBarButton() + button.configure(with: "chevron.backward", "취소") + button.moveImageToLeft() + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(cancelButtonTouched), for: .touchUpInside) + return button + }() + + private lazy var labelEditStackView: MultipleLineInputStackView = { + let viewWidth = view.frame.width + let stackViewFrame = CGRect(x: 0, y: 0, width: viewWidth, height: singleLineHeight * 3) + let stackView = MultipleLineInputStackView(frame: stackViewFrame) + + let titleInputItem = InputLineItem(category: "제목", inputView: titleTextfield) + let descriptionInputItem = InputLineItem(category: "설명", inputView: descriptionTextfield) + let backgroundColorInputItem = InputLineItem(category: "배경색", inputView: backgroundLabel) + stackView.configure(with: [titleInputItem, descriptionInputItem, backgroundColorInputItem]) + + guard let backgroundEditView = stackView.arrangedSubviews.last else { return stackView } + + backgroundEditView.addSubview(randomColorButton) + NSLayoutConstraint.activate([ + randomColorButton.widthAnchor.constraint(equalToConstant: singleLineHeight * 0.9), + randomColorButton.heightAnchor.constraint(equalTo: randomColorButton.safeAreaLayoutGuide.widthAnchor), + randomColorButton.trailingAnchor.constraint(equalTo: backgroundEditView.safeAreaLayoutGuide.trailingAnchor, constant: -spacing), + randomColorButton.centerYAnchor.constraint(equalTo: backgroundEditView.safeAreaLayoutGuide.centerYAnchor) + ]) + return stackView + }() + + private lazy var titleTextfield: UITextField = { + let textField = UITextField() + textField.placeholder = "(필수 입력)" + return textField + }() + + private lazy var descriptionTextfield: UITextField = { + let textField = UITextField() + textField.placeholder = "(선택 사항)" + return textField + }() + + private lazy var backgroundLabel: UILabel = { + let label = UILabel() + label.text = "#000000" + return label + }() + + private lazy var randomColorButton: UIButton = { + let button = UIButton() + let refreshImage = UIImage(systemName: "arrow.clockwise") + button.setImage(refreshImage, for: .normal) + button.tintColor = UIColor.black + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(randomColorButtonTouched), for: .touchUpInside) + return button + }() + + private lazy var previewLabel: LabelView = { + let labelView = LabelView() + let colorText = backgroundLabel.text ?? "#000000" + let hex = HexColorCode(from: colorText) + let titleText = titleTextfield.isEmpty() ? "레이블" : titleTextfield.text + labelView.configure(with: hex, titleText) + labelView.translatesAutoresizingMaskIntoConstraints = false + return labelView + }() + + private lazy var singleLineHeight: CGFloat = { + return view.frame.height * 0.05 + }() + + private lazy var spacing: CGFloat = { + return singleLineHeight * 0.5 + }() + + private var currentLabel: Label? + private var sceneTitle: String? + private var saveOperation: ((Label) -> Void)? + + override func viewDidLoad() { + super.viewDidLoad() + configureViews() + setTitleTextFieldSupporter() + } + + private func configureViews() { + view.backgroundColor = Colors.background + addTopMenu() + addEditStackView() + addLabelPreview() + } + + private func addTopMenu() { + view.addSubview(topMenuView) + + NSLayoutConstraint.activate([ + topMenuView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + topMenuView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + topMenuView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: spacing), + topMenuView.heightAnchor.constraint(equalToConstant: singleLineHeight) + ]) + } + + private func addEditStackView() { + view.addSubview(labelEditStackView) + + NSLayoutConstraint.activate([ + labelEditStackView.heightAnchor.constraint(equalToConstant: singleLineHeight * 3), + labelEditStackView.topAnchor.constraint(equalTo: topMenuView.safeAreaLayoutGuide.bottomAnchor, constant: 40), + labelEditStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + labelEditStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) + ]) + } + + private func addLabelPreview() { + let backgroundView = UIView() + backgroundView.backgroundColor = UIColor.systemGray5 + backgroundView.layer.cornerRadius = view.frame.width * 0.07 + backgroundView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(backgroundView) + backgroundView.addSubview(previewLabel) + + NSLayoutConstraint.activate([ + backgroundView.widthAnchor.constraint(equalToConstant: view.frame.width - spacing * 2), + backgroundView.heightAnchor.constraint(equalTo: backgroundView.safeAreaLayoutGuide.widthAnchor, multiplier: 0.75), + backgroundView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor), + backgroundView.topAnchor.constraint(equalTo: labelEditStackView.safeAreaLayoutGuide.bottomAnchor, constant: 24), + previewLabel.centerXAnchor.constraint(equalTo: backgroundView.safeAreaLayoutGuide.centerXAnchor), + previewLabel.centerYAnchor.constraint(equalTo: backgroundView.safeAreaLayoutGuide.centerYAnchor) + ]) + } + + private func setTitleTextFieldSupporter() { + titleTextfield.delegate = self + } + + func configure(withTitle sceneTitle: String, currentLabel: Label?) { + self.sceneTitle = sceneTitle + self.currentLabel = currentLabel + + setUpCurrentLabelInfo() + } + + private func setUpCurrentLabelInfo() { + guard let currentLabel = currentLabel else { return } + titleTextfield.text = currentLabel.title + descriptionTextfield.text = currentLabel.body + backgroundLabel.text = currentLabel.hexColorCode + } + + func setSaveOperation(_ operation: @escaping (Label) -> Void) { + self.saveOperation = operation + } + + @objc private func cancelButtonTouched(_ sender: UIBarButtonItem) { + dismiss(animated: true, completion: nil) + } + + @objc func saveButtonTouched(_ sender: UIBarButtonItem) { + guard let labelTitle = titleTextfield.text, + let colorCode = backgroundLabel.text, + let saveOperation = saveOperation else { return } + + let label = Label(id: currentLabel?.id ?? -1, + title: labelTitle, + body: descriptionTextfield.text ?? "", + hexColorCode: colorCode) + + saveOperation(label) + dismiss(animated: true, completion: nil) + } + + @objc private func randomColorButtonTouched(_ sender: UIButton) { + let hexColor = randomColor() + backgroundLabel.text = hexColor + changePreviewLabel() + } + + private func randomColor() -> String { + let colorRange = 0...255 + let randomRed = Int.random(in: colorRange) + let randomGreen = Int.random(in: colorRange) + let randomBlue = Int.random(in: colorRange) + + let hexRed = String(randomRed, radix: 16) + let hexGreen = String(randomGreen, radix: 16) + let hexBlue = String(randomBlue, radix: 16) + + return "#\(hexRed)\(hexGreen)\(hexBlue)" + } +} + +extension LabelControlViewController: UITextFieldDelegate { + func textFieldDidChangeSelection(_ textField: UITextField) { + changeSaveButtonEnableStatus(baseOn: textField) + changePreviewLabel() + } + + private func changeSaveButtonEnableStatus(baseOn textField: UITextField) { + DispatchQueue.main.async { + self.saveButton.isEnabled = !textField.isEmpty() + } + } + + private func changePreviewLabel() { + let hexColorString = backgroundLabel.text ?? "#000000" + let hex = HexColorCode(from: hexColorString) + let titleText = titleTextfield.isEmpty() ? "레이블" : titleTextfield.text + previewLabel.configure(with: hex, titleText) + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Label/Controller/LabelTableViewDatasource.swift b/iOS/issue-tracker/issue-tracker/Main/Label/Controller/LabelTableViewDatasource.swift new file mode 100644 index 000000000..7af081ffc --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Label/Controller/LabelTableViewDatasource.swift @@ -0,0 +1,33 @@ +// +// LabelTableViewDatasource.swift +// issue-tracker +// +// Created by Song on 2021/06/15. +// + +import UIKit + +final class LabelTableViewDatasource: NSObject, UITableViewDataSource { + + private(set) var labels = [Label]() + private let colorConverter = HexColorConverter() + + func update(labels: [Label]) { + self.labels = labels + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return labels.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cellID = LabelTableViewCell.reuseID + let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as? LabelTableViewCell ?? LabelTableViewCell() + let label = labels[indexPath.row] + let hex = HexColorCode(from: label.hexColorCode) + let backgroundColor = colorConverter.convertHex(hex) + let titleColor = colorConverter.isColorDark(hex: hex) ? UIColor.white : UIColor.black + cell.configure(with: backgroundColor, titleColor, label.title, label.body ?? "") + return cell + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Label/Controller/LabelViewController.swift b/iOS/issue-tracker/issue-tracker/Main/Label/Controller/LabelViewController.swift new file mode 100644 index 000000000..f81c11de6 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Label/Controller/LabelViewController.swift @@ -0,0 +1,191 @@ +// +// LabelViewController.swift +// issue-tracker +// +// Created by Song on 2021/06/09. +// + +import UIKit + +final class LabelViewController: UIViewController { + + private lazy var addLabelButton: ImageBarButton = { + let button = ImageBarButton() + button.configure(with: "plus", "추가") + button.addTarget(self, action: #selector(addLabelTouched), for: .touchUpInside) + return button + }() + + private lazy var labelTableView: UITableView = { + let tableView = UITableView() + let cellID = LabelTableViewCell.reuseID + tableView.register(LabelTableViewCell.self, forCellReuseIdentifier: cellID) + tableView.backgroundColor = Colors.background + tableView.allowsSelection = false + tableView.translatesAutoresizingMaskIntoConstraints = false + return tableView + }() + + private let sceneTitle = "레이블" + private let colorConverter: HexColorConvertable = HexColorConverter() + private var networkManager: NetworkManagerOperations? + private var labelTableDatasource: LabelTableViewDatasource? + private var labelTableDelegate: CommonTableDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + configureViews() + setTableViewSupporters() + setNetworkManager() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + loadLabels() + } + + private func configureViews() { + view.backgroundColor = UIColor.white + title = sceneTitle + + addNavigationButton() + addTableView() + } + + private func addNavigationButton() { + navigationItem.rightBarButtonItem = UIBarButtonItem(customView: addLabelButton) + } + + private func addTableView() { + view.addSubview(labelTableView) + + NSLayoutConstraint.activate([ + labelTableView.topAnchor.constraint(equalTo: view.topAnchor), + labelTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + labelTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + labelTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } + + private func setTableViewSupporters() { + labelTableDatasource = LabelTableViewDatasource() + labelTableView.dataSource = labelTableDatasource + + labelTableDelegate = CommonTableDelegate(cellActionHandler: swipeActionHandler, cellHeight: 118) + labelTableView.delegate = labelTableDelegate + } + + private func setNetworkManager() { + let loginInfo = LoginInfo.shared + guard let jwt = loginInfo.jwt else { return } + let headers = [Header.authorization.key(): jwt.description] + networkManager = NetworkManager(baseAddress: EndPoint.baseAddress, headers: headers) + } + + private func reloadTableView() { + DispatchQueue.main.async { + self.labelTableView.reloadData() + } + } + + private func swipeActionHandler(_ index: Int, _ action: CellAction) { + guard let targetLabel = labelTableDatasource?.labels[index] else { return } + + switch action { + case .delete: + deleteLabel(for: targetLabel.id) + case .edit: + presentEditLabelViewController(for: targetLabel) + default: + assert(false) + } + } + + private func presentEditLabelViewController(for targetLabel: Label) { + let editLabelViewController = LabelControlViewController() + editLabelViewController.configure(withTitle: "레이블 수정하기", currentLabel: targetLabel) + editLabelViewController.setSaveOperation(putEditedLabel) + editLabelViewController.modalPresentationStyle = .formSheet + + DispatchQueue.main.async { + self.present(editLabelViewController, animated: true, completion: nil) + } + } + + private func presentAlert(with errorMessage: String) { + DispatchQueue.main.async { + let alert = AlertFactory.create(body: errorMessage) + self.present(alert, animated: true, completion: nil) + } + } + + @objc private func addLabelTouched(_ sender: UIButton) { + let addLabelViewController = LabelControlViewController() + addLabelViewController.configure(withTitle: "새로운 레이블", currentLabel: nil) + addLabelViewController.setSaveOperation(postNewLabel) + addLabelViewController.modalPresentationStyle = .formSheet + + DispatchQueue.main.async { + self.present(addLabelViewController, animated: true, completion: nil) + } + } +} + +//MARK: - Network Methods +extension LabelViewController { + private func loadLabels() { + let labelListEndpoint = EndPoint.label.path() + networkManager?.get(endpoint: labelListEndpoint, queryParameters: nil, + completion: { [weak self] (result: Result, NetworkError>) in + switch result { + case .success(let result): + guard let labels = result.data else { return } + self?.labelTableDatasource?.update(labels: labels) + self?.reloadTableView() + case .failure(let error): + self?.presentAlert(with: error.description) + } + }) + } + + private func deleteLabel(for id: Int) { + let deleteLabelEndpoint = EndPoint.label.path(with: id) + networkManager?.delete(endpoint: deleteLabelEndpoint, queryParameters: nil, completion: { [weak self] (result: Result) in + switch result { + case .success(_): + self?.loadLabels() + case .failure(let error): + self?.presentAlert(with: error.description) + } + }) + } + + private func postNewLabel(_ newLabel: Label) { + let newLabelEndpoint = EndPoint.label.path() + let requestBody = NewLabelDTO(name: newLabel.title, content: newLabel.body ?? "", colorCode: newLabel.hexColorCode) + + networkManager?.post(endpoint: newLabelEndpoint, requestBody: requestBody, completion: { [weak self] result in + switch result { + case .success(_): + self?.loadLabels() + case .failure(let error): + self?.presentAlert(with: error.description) + } + }) + } + + private func putEditedLabel(_ editedLabel: Label) { + let labelId = editedLabel.id + let editLabelEndpoint = EndPoint.label.path(with: labelId) + let requestBody = NewLabelDTO(name: editedLabel.title, content: editedLabel.body ?? "", colorCode: editedLabel.hexColorCode) + + networkManager?.put(endpoint: editLabelEndpoint, requestBody: requestBody, completion: { [weak self] result in + switch result { + case .success(_): + self?.loadLabels() + case .failure(let error): + self?.presentAlert(with: error.description) + } + }) + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Label/LabelViewController.swift b/iOS/issue-tracker/issue-tracker/Main/Label/LabelViewController.swift deleted file mode 100644 index 1bac5c5a8..000000000 --- a/iOS/issue-tracker/issue-tracker/Main/Label/LabelViewController.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// LabelViewController.swift -// issue-tracker -// -// Created by Song on 2021/06/09. -// - -import UIKit - -class LabelViewController: UIViewController { - - private var loginInfo: LoginInfo? - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = UIColor.white - title = "레이블" - } - -} - -extension LabelViewController: LoginInfoContainer { - func setup(loginInfo: LoginInfo) { - self.loginInfo = loginInfo - } -} diff --git a/iOS/issue-tracker/issue-tracker/Main/Label/Model/HexColorCode.swift b/iOS/issue-tracker/issue-tracker/Main/Label/Model/HexColorCode.swift new file mode 100644 index 000000000..638490e7e --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Label/Model/HexColorCode.swift @@ -0,0 +1,31 @@ +// +// HexColorCode.swift +// issue-tracker +// +// Created by Song on 2021/06/15. +// + +import UIKit + +struct HexColorCode { + let red: CGFloat + let green: CGFloat + let blue: CGFloat + let alpha: CGFloat + + init(from hexInString: String) { + let hexNums = hexInString.filter{ $0 != "#" }.map{ String($0) } + + let hexRedString = hexNums.count >= 2 ? hexNums[0...1].joined() : "00" + self.red = CGFloat(Int(hexRedString, radix: 16) ?? 255) + + let hexGreenString = hexNums.count >= 4 ? hexNums[2...3].joined() : "00" + self.green = CGFloat(Int(hexGreenString, radix: 16) ?? 255) + + let hexBlueString = hexNums.count >= 6 ? hexNums[4...5].joined() : "00" + self.blue = CGFloat(Int(hexBlueString, radix: 16) ?? 255) + + let hexAlphaString = hexNums.count == 8 ? hexNums[6...7].joined() : "FF" + self.alpha = CGFloat(Int(hexAlphaString, radix: 16) ?? 255) + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Label/Model/HexColorConvertable.swift b/iOS/issue-tracker/issue-tracker/Main/Label/Model/HexColorConvertable.swift new file mode 100644 index 000000000..f340e7c8b --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Label/Model/HexColorConvertable.swift @@ -0,0 +1,13 @@ +// +// ColorConvertable.swift +// issue-tracker +// +// Created by Song on 2021/06/15. +// + +import UIKit + +protocol HexColorConvertable { + func convertHex(_ hex: HexColorCode) -> UIColor + func isColorDark(hex: HexColorCode) -> Bool +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Label/Model/HexColorConverter.swift b/iOS/issue-tracker/issue-tracker/Main/Label/Model/HexColorConverter.swift new file mode 100644 index 000000000..16aadd8a5 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Label/Model/HexColorConverter.swift @@ -0,0 +1,25 @@ +// +// ColorConverter.swift +// issue-tracker +// +// Created by Song on 2021/06/14. +// + +import UIKit + +final class HexColorConverter: HexColorConvertable { + func convertHex(_ hex: HexColorCode) -> UIColor { + let red = hex.red / 255 + let green = hex.green / 255 + let blue = hex.blue / 255 + let alpha = hex.alpha / 255 + return UIColor(red: red, green: green, blue: blue, alpha: alpha) + } + + func isColorDark(hex: HexColorCode) -> Bool { + let red = hex.red + let green = hex.green + let blue = hex.blue + return (red * 0.299 + green * 0.587 + blue * 0.114) <= 186 + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Label/Model/Label.swift b/iOS/issue-tracker/issue-tracker/Main/Label/Model/Label.swift new file mode 100644 index 000000000..6f70dfc25 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Label/Model/Label.swift @@ -0,0 +1,30 @@ +// +// Label.swift +// issue-tracker +// +// Created by Song on 2021/06/15. +// + +import Foundation + +protocol Identifiable { + func identifier() -> Int +} + +struct Label: Decodable, Identifiable { + private(set) var id: Int + private(set) var title: String + private(set) var body: String? + private(set) var hexColorCode: String + + enum CodingKeys: String, CodingKey { + case id + case title = "name" + case body = "content" + case hexColorCode = "color_code" + } + + func identifier() -> Int { + return id + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Label/Model/NewLabelDTO.swift b/iOS/issue-tracker/issue-tracker/Main/Label/Model/NewLabelDTO.swift new file mode 100644 index 000000000..4d1b84881 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Label/Model/NewLabelDTO.swift @@ -0,0 +1,20 @@ +// +// NewLabelDTO.swift +// issue-tracker +// +// Created by Song on 2021/06/16. +// + +import Foundation + +struct NewLabelDTO: Encodable { + let name: String + let content: String + let colorCode: String + + enum CodingKeys: String, CodingKey { + case name + case content + case colorCode = "color_code" + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Label/View/LabelTableViewCell.swift b/iOS/issue-tracker/issue-tracker/Main/Label/View/LabelTableViewCell.swift new file mode 100644 index 000000000..6708d3478 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Label/View/LabelTableViewCell.swift @@ -0,0 +1,69 @@ +// +// LabelTableViewCell.swift +// issue-tracker +// +// Created by Song on 2021/06/14. +// + +import UIKit + +final class LabelTableViewCell: UITableViewCell { + + private lazy var labelView: LabelView = LabelView() + + private lazy var labelDescription: UILabel = { + let label = UILabel() + label.textColor = UIColor.systemGray2 + label.numberOfLines = 1 + label.text = placeholder + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var spacing: CGFloat = { + return frame.height * 0.35 + }() + + private let placeholder = "No description provided" + + required init?(coder: NSCoder) { + super.init(coder: coder) + setViews() + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setViews() + } + + private func setViews() { + backgroundColor = UIColor.white + addLabelTitle() + addLabelDescription() + } + + private func addLabelTitle() { + addSubview(labelView) + + NSLayoutConstraint.activate([ + labelView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: spacing), + labelView.topAnchor.constraint(equalTo: topAnchor, constant: spacing * 1.6) + ]) + } + + private func addLabelDescription() { + addSubview(labelDescription) + + NSLayoutConstraint.activate([ + labelDescription.leadingAnchor.constraint(equalTo: leadingAnchor, constant: spacing), + labelDescription.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -spacing), + labelDescription.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -spacing * 1.6) + ]) + } + + func configure(with backgroundColor: UIColor,_ titleColor: UIColor, _ title: String,_ description: String) { + labelView.configure(with: backgroundColor, titleColor, title) + self.labelDescription.text = description.count != 0 ? description : placeholder + } + +} diff --git a/iOS/issue-tracker/issue-tracker/Main/LoginInfoContainer.swift b/iOS/issue-tracker/issue-tracker/Main/LoginInfoContainer.swift deleted file mode 100644 index 5dabea894..000000000 --- a/iOS/issue-tracker/issue-tracker/Main/LoginInfoContainer.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// LoginInfoContainer.swift -// issue-tracker -// -// Created by Song on 2021/06/10. -// - -import Foundation - -protocol LoginInfoContainer { - func setup(loginInfo: LoginInfo) -} diff --git a/iOS/issue-tracker/issue-tracker/Main/Milestone/Controller/MileStoneControlViewController.swift b/iOS/issue-tracker/issue-tracker/Main/Milestone/Controller/MileStoneControlViewController.swift new file mode 100644 index 000000000..8df239719 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Milestone/Controller/MileStoneControlViewController.swift @@ -0,0 +1,177 @@ +// +// MileStoneControlViewController.swift +// issue-tracker +// +// Created by jinseo park on 6/20/21. +// + +import Foundation +import UIKit + +class MileStoneControlViewController: UIViewController { + + private lazy var topMenuView: TopMenuView = { + let topMenuView = TopMenuView() + topMenuView.configure(withTitle: sceneTitle, rightButton: saveButton, leftButton: cancelButton) + topMenuView.translatesAutoresizingMaskIntoConstraints = false + return topMenuView + }() + + private lazy var saveButton: ImageBarButton = { + let button = ImageBarButton() + button.configure(with: "", "저장") + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(saveButtonTouched), for: .touchUpInside) + changeSaveButtonEnableStatus(baseOn: titleTextfield) + return button + }() + + private lazy var cancelButton: ImageBarButton = { + let button = ImageBarButton() + button.configure(with: "chevron.backward", "취소") + button.moveImageToLeft() + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(cancelButtonTouched), for: .touchUpInside) + return button + }() + + private lazy var mileStoneEditStackView: MultipleLineInputStackView = { + let viewWidth = view.frame.width + let stackViewFrame = CGRect(x: 0, y: 0, width: viewWidth, height: singleLineHeight * 3) + let stackView = MultipleLineInputStackView(frame: stackViewFrame) + + let titleInputItem = InputLineItem(category: "제목", inputView: titleTextfield) + let descriptionInputItem = InputLineItem(category: "설명", inputView: descriptionTextfield) + let completeDateInputItem = InputLineItem(category: "완료일", inputView: completeDateTextField) + stackView.configure(with: [titleInputItem, descriptionInputItem, completeDateInputItem]) + + return stackView + }() + + private lazy var titleTextfield: UITextField = { + let textField = UITextField() + textField.placeholder = "(필수 입력)" + return textField + }() + + private lazy var descriptionTextfield: UITextField = { + let textField = UITextField() + textField.placeholder = "(선택 사항)" + return textField + }() + + let validityType: String.ValidityType = .date + + private lazy var completeDateTextField: UITextField = { + let textField = UITextField() + textField.placeholder = "YYYY-MM-DD(선택사항)" + textField.addTarget(self, action: #selector(handleTextChange), for: .editingChanged) + return textField + }() + + private lazy var singleLineHeight: CGFloat = { + return view.frame.height * 0.05 + }() + + private lazy var spacing: CGFloat = { + return singleLineHeight * 0.5 + }() + + private var currentMileStone: MileStone? + private var sceneTitle: String? + private var saveOperation: ((MileStone) -> Void)? + + override func viewDidLoad() { + super.viewDidLoad() + configureViews() + setTitleTextFieldSupporter() + } + + @objc func handleTextChange() { + guard let text = completeDateTextField.text else { return } + switch validityType { + case .date: + if text.isValid(validityType) { + mileStoneEditStackView.setLabelColor(correct: true) + }else{ + mileStoneEditStackView.setLabelColor(correct: false) + } + } + } + + private func configureViews() { + view.backgroundColor = Colors.background + addTopMenu() + addEditStackView() + } + + private func addTopMenu() { + view.addSubview(topMenuView) + + NSLayoutConstraint.activate([ + topMenuView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + topMenuView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + topMenuView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: spacing), + topMenuView.heightAnchor.constraint(equalToConstant: singleLineHeight) + ]) + } + + private func addEditStackView() { + view.addSubview(mileStoneEditStackView) + + NSLayoutConstraint.activate([ + mileStoneEditStackView.heightAnchor.constraint(equalToConstant: singleLineHeight * 3), + mileStoneEditStackView.topAnchor.constraint(equalTo: topMenuView.safeAreaLayoutGuide.bottomAnchor, constant: 40), + mileStoneEditStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + mileStoneEditStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) + ]) + } + + private func setTitleTextFieldSupporter() { + titleTextfield.delegate = self + } + + func configure(withTitle sceneTitle: String, currentMileStone: MileStone?) { + self.sceneTitle = sceneTitle + self.currentMileStone = currentMileStone + setUpCurrentMileStoneInfo() + } + + private func setUpCurrentMileStoneInfo() { + guard let currentMileStone = currentMileStone else { return } + titleTextfield.text = currentMileStone.title + descriptionTextfield.text = currentMileStone.description + completeDateTextField.text = currentMileStone.dueDate + } + + func setSaveOperation(_ operation: @escaping (MileStone) -> Void) { + self.saveOperation = operation + } + + @objc private func cancelButtonTouched(_ sender: UIBarButtonItem) { + dismiss(animated: true, completion: nil) + } + + @objc func saveButtonTouched(_ sender: UIBarButtonItem) { + guard let mileStoneTitle = titleTextfield.text, + let completeDtae = completeDateTextField.text, + let saveOperation = saveOperation else { return } + + let mileStone = MileStone(id: currentMileStone?.id ?? -1, title: mileStoneTitle, description: descriptionTextfield.text ?? "", dueDate: completeDtae) + saveOperation(mileStone) + dismiss(animated: true, completion: nil) + } +} + +extension MileStoneControlViewController: UITextFieldDelegate { + func textFieldDidChangeSelection(_ textField: UITextField) { + changeSaveButtonEnableStatus(baseOn: textField) + } + + private func changeSaveButtonEnableStatus(baseOn textField: UITextField) { + DispatchQueue.main.async { + self.saveButton.isEnabled = !textField.isEmpty() + } + } + +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Milestone/Controller/MileStoneViewController.swift b/iOS/issue-tracker/issue-tracker/Main/Milestone/Controller/MileStoneViewController.swift new file mode 100644 index 000000000..df12e3cee --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Milestone/Controller/MileStoneViewController.swift @@ -0,0 +1,182 @@ +// +// MilestoneViewController.swift +// issue-tracker +// +// Created by Song on 2021/06/09. +// + +import UIKit + +class MileStoneViewController: UIViewController { + + private lazy var addMileStoneButton: ImageBarButton = { + let button = ImageBarButton() + button.configure(with: "plus", "추가") + button.addTarget(self, action: #selector(addMileStoneTouched), for: .touchUpInside) + return button + }() + + private lazy var mileStoneTableView: UITableView = { + let tableView = UITableView() + let cellID = MileStoneTableViewCell.reuseID + tableView.register(MileStoneTableViewCell.self, forCellReuseIdentifier: cellID) + tableView.backgroundColor = Colors.background + tableView.translatesAutoresizingMaskIntoConstraints = false + return tableView + }() + + @objc private func addMileStoneTouched(_ sender: UIButton) { + let addMileStoneViewController = MileStoneControlViewController() + addMileStoneViewController.configure(withTitle: "새로운 마일스톤", currentMileStone: nil) + addMileStoneViewController.setSaveOperation(postNewMileStone) + addMileStoneViewController.modalPresentationStyle = .formSheet + + DispatchQueue.main.async { + self.present(addMileStoneViewController, animated: true, completion: nil) + } + } + + private var networkManager: NetworkManagerOperations? + private var mileStoneTableDatasource: MilestoneTableViewDataSource? + private var mileStoneTableDelegate: CommonTableDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = UIColor.white + title = "마일스톤" + addNavigationButton() + addTableView() + setTableViewSupporters() + setNetworkManager() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + loadMileStones() + } + + private func addNavigationButton() { + navigationItem.rightBarButtonItem = UIBarButtonItem(customView: addMileStoneButton) + } + + private func addTableView() { + view.addSubview(mileStoneTableView) + + NSLayoutConstraint.activate([ + mileStoneTableView.topAnchor.constraint(equalTo: view.topAnchor), + mileStoneTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + mileStoneTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + mileStoneTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } + + private func setTableViewSupporters() { + mileStoneTableDatasource = MilestoneTableViewDataSource() + mileStoneTableView.dataSource = mileStoneTableDatasource + + mileStoneTableDelegate = CommonTableDelegate(cellActionHandler: swipeActionHandler, cellHeight: 198) + mileStoneTableView.delegate = mileStoneTableDelegate + } + + private func setNetworkManager() { + let loginInfo = LoginInfo.shared + guard let jwt = loginInfo.jwt else { return } + let headers = [Header.authorization.key(): jwt.description] + networkManager = NetworkManager(baseAddress: EndPoint.baseAddress, headers: headers) + } + + private func presentAlert(with errorMessage: String) { + DispatchQueue.main.async { + let alert = AlertFactory.create(body: errorMessage) + self.present(alert, animated: true, completion: nil) + } + } + + private func reloadTableView() { + DispatchQueue.main.async { + self.mileStoneTableView.reloadData() + } + } + private func swipeActionHandler(_ index: Int, _ action: CellAction) { + guard let targetMileStone = mileStoneTableDatasource?.milestones[index] else { return } + + switch action { + case .edit: + presentEditMileStoneViewController(for: targetMileStone) + case .delete: + deleteMileStone(for: targetMileStone.id) + default: + assert(false) + } + } + + private func presentEditMileStoneViewController(for targetMileStone: MileStone) { + let editMileStoneViewController = MileStoneControlViewController() + editMileStoneViewController.configure(withTitle: "마일스톤 수정하기", currentMileStone: targetMileStone) + editMileStoneViewController.setSaveOperation(putEditedMileStone) + editMileStoneViewController.modalPresentationStyle = .formSheet + + DispatchQueue.main.async { + self.present(editMileStoneViewController, animated: true, completion: nil) + } + } +} + +//MARK: - Network Methods +extension MileStoneViewController { + private func loadMileStones() { + let mileStoneListEndpoint = + EndPoint.milestone.path() + networkManager?.get(endpoint: mileStoneListEndpoint, queryParameters: nil, completion: { [weak self] (result: Result, NetworkError>) in + switch result { + case .success(let result): + guard let mileStone = result.data else { return } + self?.mileStoneTableDatasource?.update(milestones: mileStone) + self?.reloadTableView() + case .failure(let error): + self?.presentAlert(with: error.description) + } + }) + } + + private func deleteMileStone(for id: Int) { + let deleteMileStoneEndpoint = EndPoint.milestone.path(with: id) + networkManager?.delete(endpoint: deleteMileStoneEndpoint, queryParameters: nil, completion: { [weak self] (result: Result) in + switch result { + case .success(_): + self?.loadMileStones() + case .failure(let error): + self?.presentAlert(with: error.description) + } + }) + } + + private func postNewMileStone(_ newMileStone: MileStone) { + let newMileStoneEndpoint = EndPoint.milestone.path() + let requestBody = NewMileStoneDTO(title: newMileStone.title, description: newMileStone.description ?? "", dueDate: newMileStone.dueDate ?? "") + + networkManager?.post(endpoint: newMileStoneEndpoint, requestBody: requestBody, completion: { [weak self] result in + switch result { + case .success(_): + self?.loadMileStones() + case .failure(let error): + self?.presentAlert(with: error.description) + } + }) + } + + private func putEditedMileStone(_ editedMileStone: MileStone) { + let mileStoneId = editedMileStone.id + let editMileStoneEndpoint = EndPoint.milestone.path(with: mileStoneId) + let requestBody = NewMileStoneDTO(title: editedMileStone.title, description: editedMileStone.description ?? "", dueDate: editedMileStone.dueDate ?? "") + + networkManager?.put(endpoint: editMileStoneEndpoint, requestBody: requestBody, completion: { [weak self] result in + switch result { + case .success(_): + self?.loadMileStones() + case .failure(let error): + self?.presentAlert(with: error.description) + } + }) + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Milestone/Controller/MilestoneTableViewDataSource.swift b/iOS/issue-tracker/issue-tracker/Main/Milestone/Controller/MilestoneTableViewDataSource.swift new file mode 100644 index 000000000..9683978f4 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Milestone/Controller/MilestoneTableViewDataSource.swift @@ -0,0 +1,31 @@ +// +// MilestoneTableViewDataSource.swift +// issue-tracker +// +// Created by jinseo park on 6/18/21. +// + +import UIKit + +class MilestoneTableViewDataSource: NSObject, UITableViewDataSource { + + private(set) var milestones = [MileStone]() + + func update(milestones: [MileStone]) { + self.milestones = milestones + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return milestones.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cellID = MileStoneTableViewCell.reuseID + let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as? MileStoneTableViewCell ?? MileStoneTableViewCell() + + let milestone = milestones[indexPath.row] + + cell.configure(title: milestone.title, description: milestone.description ?? "", dueDate: milestone.dueDate ?? "") + return cell + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Milestone/MilestoneViewController.swift b/iOS/issue-tracker/issue-tracker/Main/Milestone/MilestoneViewController.swift index 3402dc85e..a7e26549c 100644 --- a/iOS/issue-tracker/issue-tracker/Main/Milestone/MilestoneViewController.swift +++ b/iOS/issue-tracker/issue-tracker/Main/Milestone/MilestoneViewController.swift @@ -7,10 +7,7 @@ import UIKit -class MilestoneViewController: UIViewController { - - private var loginInfo: LoginInfo? - +class MilestoneViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = UIColor.yellow @@ -18,9 +15,3 @@ class MilestoneViewController: UIViewController { } } - -extension MilestoneViewController: LoginInfoContainer { - func setup(loginInfo: LoginInfo) { - self.loginInfo = loginInfo - } -} diff --git a/iOS/issue-tracker/issue-tracker/Main/Milestone/Model/MileStoneLabelView.swift b/iOS/issue-tracker/issue-tracker/Main/Milestone/Model/MileStoneLabelView.swift new file mode 100644 index 000000000..e1fd606ac --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Milestone/Model/MileStoneLabelView.swift @@ -0,0 +1,83 @@ +// +// mileStoneLabelView.swift +// issue-tracker +// +// Created by jinseo park on 6/16/21. +// + +import UIKit + +final class MileStoneLabelView: UIView { + + var text: String? + var fontColor: UIColor? + var bgColor: UIColor? + var imgName: String? + var issueCount: Int? + + private lazy var labelTitle: UILabel = { + let label = UILabel() + + label.translatesAutoresizingMaskIntoConstraints = false + + guard let imgName = self.imgName else { return UILabel() } + guard let fontColors = self.fontColor else { return UILabel() } + guard let texts = self.text else { return UILabel() } + guard let issueCount = self.issueCount else { return UILabel() } + + let attributedString = NSMutableAttributedString(string: "") + let imageAttachment = NSTextAttachment() + let attrs = [NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 13), NSAttributedString.Key.foregroundColor : fontColors] + let issueCountString = NSMutableAttributedString(string:"\(texts) \(issueCount)개", attributes:attrs) + + imageAttachment.image = UIImage(systemName: imgName) + attributedString.append(NSAttributedString(attachment: imageAttachment)) + attributedString.append(issueCountString) + label.attributedText = attributedString + + return label + }() + + private let spacing: CGFloat = 15 + + private lazy var labelHeight: CGFloat = { + return spacing * 2 + }() + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init(text: String, fontColor: UIColor, bgColor: UIColor, imgName: String, issueCount: Int) { + super.init(frame: .zero) + self.text = text + self.fontColor = fontColor + self.bgColor = bgColor + self.imgName = imgName + self.issueCount = issueCount + configure() + } + + private func configure() { + layer.cornerRadius = labelHeight * 0.5 + translatesAutoresizingMaskIntoConstraints = false + addLabelTitle() + backgroundColor = bgColor + } + + private func addLabelTitle() { + addSubview(labelTitle) + NSLayoutConstraint.activate([ + labelTitle.centerXAnchor.constraint(equalTo: centerXAnchor), + labelTitle.centerYAnchor.constraint(equalTo: centerYAnchor), + widthAnchor.constraint(equalTo: widthAnchor), + heightAnchor.constraint(equalToConstant: labelHeight) + ]) + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Milestone/Model/Milestone.swift b/iOS/issue-tracker/issue-tracker/Main/Milestone/Model/Milestone.swift new file mode 100644 index 000000000..624c8dc34 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Milestone/Model/Milestone.swift @@ -0,0 +1,26 @@ +// +// Milestone.swift +// issue-tracker +// +// Created by jinseo park on 6/18/21. +// + +import Foundation + +struct MileStone: Decodable, Identifiable { + let id: Int + let title: String + let description: String? + let dueDate: String? + + enum CodingKeys: String, CodingKey { + case id + case title + case description + case dueDate = "due_date" + } + + func identifier() -> Int { + return id + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Milestone/Model/NewMileStoneDTO.swift b/iOS/issue-tracker/issue-tracker/Main/Milestone/Model/NewMileStoneDTO.swift new file mode 100644 index 000000000..bd922e081 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Milestone/Model/NewMileStoneDTO.swift @@ -0,0 +1,21 @@ +// +// NewMileStoneDTO.swift +// issue-tracker +// +// Created by jinseo park on 6/20/21. +// + +import Foundation + +struct NewMileStoneDTO: Encodable { + + let title: String + let description: String + let dueDate: String + + enum CodingKeys: String, CodingKey { + case title + case description + case dueDate = "due_date" + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/Milestone/View/MileStoneTableViewCell.swift b/iOS/issue-tracker/issue-tracker/Main/Milestone/View/MileStoneTableViewCell.swift new file mode 100644 index 000000000..1dd1582e3 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Main/Milestone/View/MileStoneTableViewCell.swift @@ -0,0 +1,172 @@ +// +// MileStoneTableViewCell.swift +// issue-tracker +// +// Created by jinseo park on 6/15/21. +// + +import UIKit + +class MileStoneTableViewCell: UITableViewCell { + + private let spacing: CGFloat = 16 + private lazy var mileStoneStackView: UIStackView = { + let superStackView = UIStackView() + superStackView.axis = .vertical + superStackView.distribution = .fillProportionally + superStackView.translatesAutoresizingMaskIntoConstraints = false + superStackView.spacing = 1 + return superStackView + }() + + private lazy var firstSubStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.distribution = .fill + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.text = "제목" + label.font = UIFont.boldSystemFont(ofSize: 22) + return label + }() + + private lazy var completenessLabel: UILabel = { + let label = UILabel() + label.text = "100%" + label.textColor = Colors.mileStoneSuceess + label.font = UIFont.boldSystemFont(ofSize: 22) + return label + }() + + private lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.textColor = Colors.description + label.text = "마일스톤에 대한 설명(한 줄만 보여짐, 생략 가능)" + label.numberOfLines = 1 + label.font = .systemFont(ofSize: 17) + return label + }() + + private lazy var dateLabel: UILabel = { + let label = UILabel() + return label + }() + + private lazy var milestoneLabelSubStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.distribution = .fillEqually + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private lazy var openMilestoneLabelView: MileStoneLabelView = { + let milestoneLabelView = MileStoneLabelView(text: "열린 이슈", fontColor: Colors.openMileStoneTint, bgColor: Colors.openMileStoneBG, imgName: "exclamationmark.circle", issueCount: 0) + return milestoneLabelView + }() + + private lazy var closeMilestoneLabelView: MileStoneLabelView = { + let milestoneLabelView = MileStoneLabelView(text: "닫힌 이슈", fontColor: Colors.closeMileStoneTint, bgColor: Colors.closeMileStoneBG, imgName: "archivebox", issueCount: 0) + return milestoneLabelView + }() + + required init?(coder: NSCoder) { + super.init(coder: coder) + setViews() + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setViews() + } + + private func setViews() { + addMileStoneStackView() + addMileStoneFirstStackView() + addMileStoneSecondStackView() + addMileStoneThirdStackView() + addMileStoneFourthStackView() + } + + private func addMileStoneStackView() { + addSubview(mileStoneStackView) + NSLayoutConstraint.activate([ + mileStoneStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + mileStoneStackView.topAnchor.constraint(equalTo: topAnchor, constant: 24), + mileStoneStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -24), + mileStoneStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16) + ]) + } + + func addMileStoneFirstStackView() { + mileStoneStackView.addArrangedSubview(firstSubStackView) + + NSLayoutConstraint.activate([ + firstSubStackView.leadingAnchor.constraint(equalTo: mileStoneStackView.leadingAnchor), + firstSubStackView.trailingAnchor.constraint(equalTo: mileStoneStackView.trailingAnchor) + ]) + + firstSubStackView + .addArrangedSubview(titleLabel) + firstSubStackView + .addArrangedSubview(completenessLabel) + + firstSubStackView.spacing = 16 + completenessLabel.textAlignment = .right + + NSLayoutConstraint.activate([ + titleLabel.widthAnchor.constraint(greaterThanOrEqualTo: firstSubStackView.widthAnchor, multiplier: 0.6), + completenessLabel.widthAnchor.constraint(equalTo: firstSubStackView.widthAnchor, multiplier: 0.2) + ]) + + } + + func addMileStoneSecondStackView() { + mileStoneStackView.addArrangedSubview(descriptionLabel) + } + + func addMileStoneThirdStackView() { + mileStoneStackView.addArrangedSubview(dateLabel) + } + + func addMileStoneFourthStackView() { + mileStoneStackView.addArrangedSubview(milestoneLabelSubStackView) + + NSLayoutConstraint.activate([ + milestoneLabelSubStackView.leadingAnchor.constraint(equalTo: mileStoneStackView.leadingAnchor), + milestoneLabelSubStackView.trailingAnchor.constraint(equalTo: mileStoneStackView.trailingAnchor) + ]) + + milestoneLabelSubStackView + .addArrangedSubview(openMilestoneLabelView) + milestoneLabelSubStackView + .addArrangedSubview(closeMilestoneLabelView) + + milestoneLabelSubStackView.spacing = 4 + + } + + func configure(title: String, description: String, dueDate: String) { + titleLabel.text = title + descriptionLabel.text = description + dueDateConfigure(dueDate: dueDate) + } + + private func dueDateConfigure(dueDate: String) { + let dateText = dueDate != "" ? dueDate :"완료일(생략가능)" + let attributedString = NSMutableAttributedString(string: "") + let imageAttachment = NSTextAttachment() + imageAttachment.image = UIImage(systemName: "calendar") + + let attrs = [NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 18), NSAttributedString.Key.foregroundColor : Colors.description] + let dateString = NSMutableAttributedString(string:dateText, attributes:attrs) + + attributedString.append(NSAttributedString(attachment: imageAttachment)) + attributedString.append(dateString) + dateLabel.attributedText = attributedString + } +} diff --git a/iOS/issue-tracker/issue-tracker/Main/MyAccount/MyAccountViewController.swift b/iOS/issue-tracker/issue-tracker/Main/MyAccount/MyAccountViewController.swift index 4136b6b40..8544eda5c 100644 --- a/iOS/issue-tracker/issue-tracker/Main/MyAccount/MyAccountViewController.swift +++ b/iOS/issue-tracker/issue-tracker/Main/MyAccount/MyAccountViewController.swift @@ -32,17 +32,22 @@ class MyAccountViewController: UIViewController { private var userName = "unknown" private let spacing: CGFloat = 16 - private var loginInfo: LoginInfo? override func viewDidLoad() { - super.viewDidLoad() view.backgroundColor = .white title = "내 계정" + updateWelcomeLabel() addWelcomeLabel() addLogoutButton() } + private func updateWelcomeLabel() { + let loginInfo = LoginInfo.shared + guard let userName = loginInfo.name else { return } + self.userName = userName + } + private func addWelcomeLabel() { view.addSubview(welcomeLabel) @@ -72,35 +77,17 @@ class MyAccountViewController: UIViewController { } @objc private func didLogoutTouched(_ sender: UIButton) { - var loginManager: LoginKeyChainManager? - for loginService in LoginService.allCases { - loginManager = LoginKeyChainManager(loginService: loginService) - loginInfo = loginManager?.read() - if loginInfo != nil { - break - } + let loginInfo = LoginInfo.shared + guard let service = loginInfo.service else { return } + let loginManager = LoginKeyChainManager(loginService: service) + guard loginManager.delete() else { + let logoutError = LoginError.logout + presentAlert(with: logoutError.description) + return } - guard let loginManager = loginManager else { return } - let _ = loginManager.delete() - + loginInfo.clear() let loginViewController = LoginViewController() loginViewController.modalPresentationStyle = .fullScreen self.present(loginViewController, animated: true, completion: nil) - } - -} - -extension MyAccountViewController: LoginInfoContainer { - - func setup(loginInfo: LoginInfo) { - self.loginInfo = loginInfo - updateWelcomeLabel() - } - - private func updateWelcomeLabel() { - guard let userName = loginInfo?.name else { return } - self.userName = userName - } - } diff --git a/iOS/issue-tracker/issue-tracker/Main/TabBar/IssueTrackerTabBarController.swift b/iOS/issue-tracker/issue-tracker/Main/TabBar/IssueTrackerTabBarController.swift index c5e2f0a74..1b0b70f80 100644 --- a/iOS/issue-tracker/issue-tracker/Main/TabBar/IssueTrackerTabBarController.swift +++ b/iOS/issue-tracker/issue-tracker/Main/TabBar/IssueTrackerTabBarController.swift @@ -8,8 +8,7 @@ import UIKit class IssueTrackerTabBarController: UITabBarController { - - private var loginInfo: LoginInfo? + private let imageLoadManager = ImageLoadManager() override func viewDidLoad() { @@ -20,19 +19,16 @@ class IssueTrackerTabBarController: UITabBarController { tabBar.tintColor = Colors.mainGrape super.viewDidAppear(animated) } - + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() updateUserImage() } - - func configure(loginInfo: LoginInfo) { - self.loginInfo = loginInfo - } - + private func updateUserImage() { - - guard let imageURL = self.loginInfo?.avatarURL else { return } + let loginInfo = LoginInfo.shared + guard let imageURL = loginInfo.avatarURL else { return } + imageLoadManager.load(from: imageURL) { cachePath in guard let userImage = UIImage(contentsOfFile: cachePath) else {return} DispatchQueue.main.async { diff --git a/iOS/issue-tracker/issue-tracker/Main/TabBar/IssueTrackerTabBarCreator.swift b/iOS/issue-tracker/issue-tracker/Main/TabBar/IssueTrackerTabBarCreator.swift index 8951d817a..e53d39352 100644 --- a/iOS/issue-tracker/issue-tracker/Main/TabBar/IssueTrackerTabBarCreator.swift +++ b/iOS/issue-tracker/issue-tracker/Main/TabBar/IssueTrackerTabBarCreator.swift @@ -10,7 +10,6 @@ import UIKit final class IssueTrackerTabBarCreator { private let childInfos: [TabBarChildInfo] - private var loginInfo: LoginInfo enum Title { static let issue = "이슈" @@ -26,17 +25,16 @@ final class IssueTrackerTabBarCreator { static let myAccount = "person.circle" } - init(childInfos: [TabBarChildInfo], loginInfo: LoginInfo) { + init(childInfos: [TabBarChildInfo]) { self.childInfos = childInfos - self.loginInfo = loginInfo } - convenience init(loginInfo: LoginInfo) { + convenience init() { let issue = TabBarChildInfo(title: Title.issue, imageName: SystemImageName.issue, type: IssueViewController.self) let label = TabBarChildInfo(title: Title.label, imageName: SystemImageName.label, type: LabelViewController.self) - let milestone = TabBarChildInfo(title: Title.milestone, imageName: SystemImageName.milestone, type: MilestoneViewController.self) + let milestone = TabBarChildInfo(title: Title.milestone, imageName: SystemImageName.milestone, type: MileStoneViewController.self) let myAccount = TabBarChildInfo(title: Title.myAccount, imageName: SystemImageName.myAccount, type: MyAccountViewController.self) - self.init(childInfos: [issue, label, milestone, myAccount], loginInfo: loginInfo) + self.init(childInfos: [issue, label, milestone, myAccount]) } private func generateChild(with info: TabBarChildInfo) -> UIViewController { @@ -44,10 +42,6 @@ final class IssueTrackerTabBarCreator { let childViewController = info.type.create() childViewController.tabBarItem = tabBarItem - if let loginInfoContainer = childViewController as? LoginInfoContainer { - loginInfoContainer.setup(loginInfo: self.loginInfo) - } - let navigationController = UINavigationController() navigationController.navigationBar.prefersLargeTitles = true navigationController.pushViewController(childViewController, animated: false) @@ -59,7 +53,6 @@ final class IssueTrackerTabBarCreator { extension IssueTrackerTabBarCreator { func create() -> IssueTrackerTabBarController { let issueTrackerTabBarController = IssueTrackerTabBarController() - issueTrackerTabBarController.configure(loginInfo: loginInfo) let childs = childInfos.map{ generateChild(with: $0) } issueTrackerTabBarController.setViewControllers(childs, animated: true) return issueTrackerTabBarController diff --git a/iOS/issue-tracker/issue-tracker/Network/EndPoint.swift b/iOS/issue-tracker/issue-tracker/Network/EndPoint.swift new file mode 100644 index 000000000..6f248af0a --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Network/EndPoint.swift @@ -0,0 +1,40 @@ +// +// EndPoint.swift +// issue-tracker +// +// Created by Song on 2021/06/15. +// + +import Foundation + +enum EndPoint { + static let baseAddress = "http://3.34.122.67/api" + + case OAuth + case issue + case label + case milestone + case user + case none + + func path() -> String { + switch self { + case .OAuth: + return "/login/ios" + case .issue: + return "/issues" + case .label: + return "/labels" + case .milestone: + return "/milestones" + case .user: + return "/users" + case .none: + return "" + } + } + + func path(with id: Int) -> String { + return path() + "/\(id)" + } +} diff --git a/iOS/issue-tracker/issue-tracker/Network/NetworkError.swift b/iOS/issue-tracker/issue-tracker/Network/NetworkError.swift new file mode 100644 index 000000000..7efe8b0a6 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Network/NetworkError.swift @@ -0,0 +1,33 @@ +// +// NetworkError.swift +// issue-tracker +// +// Created by Song on 2021/06/16. +// + +import Foundation + +enum NetworkError: Error { + case internet + case noResult + case notAllowed + case server + case unknown +} + +extension NetworkError: CustomStringConvertible { + var description: String { + switch self { + case .internet: + return "인터넷 연결을 확인해주세요 :(" + case .noResult: + return "검색 결과를 찾을 수 없습니다 :(" + case .notAllowed: + return "잘못된 접근입니다 :(" + case .server: + return "서버 상태가 불안정합니다 :(" + case .unknown: + return "알 수 없는 문제가 발생했습니다 :(" + } + } +} diff --git a/iOS/issue-tracker/issue-tracker/Network/NetworkManager.swift b/iOS/issue-tracker/issue-tracker/Network/NetworkManager.swift index f43a6d0d9..39a2e41e7 100644 --- a/iOS/issue-tracker/issue-tracker/Network/NetworkManager.swift +++ b/iOS/issue-tracker/issue-tracker/Network/NetworkManager.swift @@ -9,27 +9,76 @@ import Foundation import Alamofire protocol NetworkManagerOperations { - func setInfoGithub(with code: String, completion: @escaping (Result) -> Void) + func get(endpoint: String, queryParameters: [String: Any]?, completion: @escaping (Result) -> Void) + func delete(endpoint: String, queryParameters: [String: Any]?, completion: @escaping (Result) -> Void) + func post(endpoint: String, requestBody: T, completion: @escaping (Result) -> Void) + func put(endpoint: String, requestBody: T, completion: @escaping (Result) -> Void) } -class NetworkManager: NetworkManagerOperations { +final class NetworkManager: NetworkManagerOperations { - let accessTokenURL = "http://3.34.122.67/api/login/ios" + private let requestManager: RequestManager - func setInfoGithub(with code: String, completion: @escaping (Result) -> Void) { - - let param: Parameters = [ - "code" : code - ] + init(baseAddress: String, headers: [String: String]? = nil) { + let requestManager = RequestManager(baseAddress: baseAddress, headers: headers) + self.requestManager = requestManager + } + + func get(endpoint: String, queryParameters: [String: Any]?, completion: @escaping (Result) -> Void) { + let request = requestManager.create(endpoint: endpoint, method: .get, queryParameters: queryParameters) - AF.request(accessTokenURL, method: .get, parameters: param) - .responseDecodable(of: T.self) { response in + request.responseDecodable(of: T.self) { [weak self] response in switch response.result { case .success(let data): completion(.success(data)) - case .failure(let error): - print(error.localizedDescription) + case .failure(_): + let statusCode = response.response?.statusCode + let error = self?.networkError(for: statusCode) ?? NetworkError.unknown + completion(.failure(error)) } - } + } + } + + func delete(endpoint: String, queryParameters: [String: Any]?, completion: @escaping (Result) -> Void) { + let request = requestManager.create(endpoint: endpoint, method: .delete, queryParameters: queryParameters) + executeVoidRequest(request: request, completion: completion) + } + + func post(endpoint: String, requestBody: T, completion: @escaping (Result) -> Void) { + let request = requestManager.create(endpoint: endpoint, method: .post, encodableParameters: requestBody) + executeVoidRequest(request: request, completion: completion) + } + + func put(endpoint: String, requestBody: T, completion: @escaping (Result) -> Void) { + let request = requestManager.create(endpoint: endpoint, method: .put, encodableParameters: requestBody) + executeVoidRequest(request: request, completion: completion) + } + + private func executeVoidRequest(request: DataRequest, completion: @escaping (Result) -> Void) { + request.response { [weak self] response in + switch response.result { + case .success(_): + completion(.success(())) + case .failure(_): + let statusCode = response.response?.statusCode + let error = self?.networkError(for: statusCode) ?? NetworkError.unknown + completion(.failure(error)) + } + } + } + + private func networkError(for statusCode: Int?) -> NetworkError { + guard let statusCode = statusCode else { return NetworkError.internet } + + switch statusCode { + case 300..<400: + return NetworkError.noResult + case 400..<500: + return NetworkError.notAllowed + case 500...: + return NetworkError.server + default: + return NetworkError.unknown + } } } diff --git a/iOS/issue-tracker/issue-tracker/Network/RequestKeys.swift b/iOS/issue-tracker/issue-tracker/Network/RequestKeys.swift new file mode 100644 index 000000000..410009b44 --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Network/RequestKeys.swift @@ -0,0 +1,30 @@ +// +// RequestKeys.swift +// issue-tracker +// +// Created by Song on 2021/06/15. +// + +import Foundation + +enum Parameter { + case code + + func key() -> String { + switch self { + case .code: + return "code" + } + } +} + +enum Header { + case authorization + + func key() -> String { + switch self { + case .authorization: + return "Authorization" + } + } +} diff --git a/iOS/issue-tracker/issue-tracker/Network/RequestManager.swift b/iOS/issue-tracker/issue-tracker/Network/RequestManager.swift new file mode 100644 index 000000000..ae4a2196b --- /dev/null +++ b/iOS/issue-tracker/issue-tracker/Network/RequestManager.swift @@ -0,0 +1,39 @@ +// +// RequestManager.swift +// issue-tracker +// +// Created by Song on 2021/06/15. +// + +import Foundation +import Alamofire + +final class RequestManager { + + private var baseAddress: String + private var headers: HTTPHeaders? + private let successCodeRange = 200..<300 + + init(baseAddress: String, headers: [String: String]? = nil) { + self.baseAddress = baseAddress + self.headers = HTTPHeaders(headers ?? [:]) + } + + func create(endpoint: String = "", method: HTTPMethod, queryParameters: [String: Any]?) -> DataRequest { + return AF.request(self.baseAddress + endpoint, + method: method, + parameters: queryParameters, + encoding: URLEncoding.queryString, + headers: self.headers) + .validate(statusCode: successCodeRange) + } + + func create(endpoint: String = "", method: HTTPMethod, encodableParameters: T) -> DataRequest { + return AF.request(self.baseAddress + endpoint, + method: method, + parameters: encodableParameters, + encoder: JSONParameterEncoder(), + headers: self.headers) + .validate(statusCode: successCodeRange) + } +}