diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84bb4d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ + +## OS X files +.DS_Store +.DS_Store? +.Trashes +.Spotlight-V100 +*.swp + +## Xcode build files +DerivedData/ +build/ + +## Xcode private settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +xcuserdata/ + +## Other +*.xccheckout +*.moved-aside +*.xcuserstate +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## Swift Package Manager +.build/ + +Pods/ +Podfile.lock \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..54c3258 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,44 @@ + +disabled_rules: # 실행에서 제외할 룰 식별자들 + - colon + - comma + - control_statement + - unused_optional_binding + - trailing_whitespace + - discarded_notification_center_observer +opt_in_rules: # 일부 룰은 옵트 인 형태로 제공 + - empty_count + # 사용 가능한 모든 룰은 swiftlint rules 명령으로 확인 가능 +included: # 린트 과정에 포함할 파일 경로. 이 항목이 존재하면 `--path`는 무시됨 + - GithubIssues +excluded: # 린트 과정에서 무시할 파일 경로. `included`보다 우선순위 높음 + - Pods + +# 설정 가능한 룰은 이 설정 파일에서 커스터마이징 가능 +# 경고나 에러 중 하나를 발생시키는 룰은 위반 수준을 설정 가능 +force_cast: warning # 암시적으로 지정 +force_try: + severity: warning # 명시적으로 지정 +# 경고 및 에러 둘 다 존재하는 룰의 경우 값을 하나만 지정하면 암시적으로 경고 수준에 설정됨 +line_length: 280 + +# 둘 다 명시적으로 지정할 수도 있음 +file_length: + warning: 500 + error: 1200 +# 네이밍 룰은 경고/에러에 min_length와 max_length를 각각 설정 가능 +# 제외할 이름을 설정할 수 있음 +type_name: + min_length: 3 # 경고에만 적용됨 + max_length: # 경고와 에러 둘 다 적용 + warning: 40 + error: 50 + excluded: iPhone # 제외할 문자열 값 사용 +identifier_name: + min_length: # min_length에서 + error: 3 # 에러만 적용 + excluded: # 제외할 문자열 목록 사용 + - id + - URL + - GlobalAPIKey +reporter: "xcode" # 보고 유형 (xcode, json, csv, checkstyle, junit, html, emoji) \ No newline at end of file diff --git a/GithubIssues.xcodeproj/project.pbxproj b/GithubIssues.xcodeproj/project.pbxproj index 965c4eb..4e172c7 100644 --- a/GithubIssues.xcodeproj/project.pbxproj +++ b/GithubIssues.xcodeproj/project.pbxproj @@ -7,13 +7,43 @@ objects = { /* Begin PBXBuildFile section */ + C9911DA0C2D7555BCE11A163 /* Pods_GithubIssues.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE8C82EDEECF19666BE564C8 /* Pods_GithubIssues.framework */; }; + D7014DC61F6814A000F0F7F7 /* IssueDetailHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7014DC51F6814A000F0F7F7 /* IssueDetailHeaderCell.swift */; }; + D7014DC81F6814ED00F0F7F7 /* UIColor+extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7014DC71F6814ED00F0F7F7 /* UIColor+extension.swift */; }; + D71E7FD11F7FADF800FD5DA0 /* BitbucketRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71E7FD01F7FADF800FD5DA0 /* BitbucketRouter.swift */; }; + D71E7FD31F7FAE4C00FD5DA0 /* GithubAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71E7FD21F7FAE4C00FD5DA0 /* GithubAPI.swift */; }; + D71E7FD51F7FAE6100FD5DA0 /* BitbucketAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71E7FD41F7FAE6100FD5DA0 /* BitbucketAPI.swift */; }; + D7412BD31F701F4A00E63DF6 /* IssueCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D7412BD21F701F4A00E63DF6 /* IssueCell.xib */; }; + D776DCC51F76286300E61284 /* LoadMoreCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D776DCC41F76286300E61284 /* LoadMoreCell.xib */; }; + D776DD041F77E3D900E61284 /* IssueDetailHeaderCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D776DD031F77E3D900E61284 /* IssueDetailHeaderCell.xib */; }; + D7CB7A351F74A3C700816E33 /* ListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CB7A341F74A3C700816E33 /* ListViewController.swift */; }; + D7CB7A371F74A41900816E33 /* LoadMoreCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CB7A361F74A41900816E33 /* LoadMoreCell.swift */; }; D7CE58DE1F64118400380CEE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CE58DD1F64118400380CEE /* AppDelegate.swift */; }; - D7CE58E01F64118400380CEE /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CE58DF1F64118400380CEE /* ViewController.swift */; }; D7CE58E31F64118400380CEE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D7CE58E11F64118400380CEE /* Main.storyboard */; }; D7CE58E51F64118400380CEE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D7CE58E41F64118400380CEE /* Assets.xcassets */; }; D7CE58E81F64118400380CEE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D7CE58E61F64118400380CEE /* LaunchScreen.storyboard */; }; D7CE58F31F64118400380CEE /* GithubIssuesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CE58F21F64118400380CEE /* GithubIssuesTests.swift */; }; D7CE58FE1F64118400380CEE /* GithubIssuesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CE58FD1F64118400380CEE /* GithubIssuesUITests.swift */; }; + D7CE590C1F64127C00380CEE /* GithubRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CE590B1F64127C00380CEE /* GithubRouter.swift */; }; + D7CE59101F64CB6F00380CEE /* DataRequest+extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CE590F1F64CB6F00380CEE /* DataRequest+extension.swift */; }; + D7CE59131F64CBD800380CEE /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CE59121F64CBD800380CEE /* API.swift */; }; + D7CE59151F64CC0B00380CEE /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CE59141F64CC0B00380CEE /* Model.swift */; }; + D7CE59181F64CC6B00380CEE /* Issue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CE59171F64CC6B00380CEE /* Issue.swift */; }; + D7CE591A1F64CFD500380CEE /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CE59191F64CFD500380CEE /* User.swift */; }; + D7CE591D1F64D82E00380CEE /* IssuesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CE591C1F64D82E00380CEE /* IssuesViewController.swift */; }; + D7CE591F1F64D99300380CEE /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CE591E1F64D99300380CEE /* LoginViewController.swift */; }; + D7CE59211F64DBB800380CEE /* GlobalState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CE59201F64DBB800380CEE /* GlobalState.swift */; }; + D7CE59241F65005D00380CEE /* RepoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CE59231F65005D00380CEE /* RepoViewController.swift */; }; + D7CE59261F65077B00380CEE /* IssueCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CE59251F65077B00380CEE /* IssueCell.swift */; }; + D7CE59281F65209A00380CEE /* LayoutAttributable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CE59271F65209A00380CEE /* LayoutAttributable.swift */; }; + D7CE592A1F6546D200380CEE /* IssueDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CE59291F6546D200380CEE /* IssueDetailViewController.swift */; }; + D7CE592C1F655AF600380CEE /* IssueCommentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CE592B1F655AF600380CEE /* IssueCommentCell.swift */; }; + D7CE592E1F655C1C00380CEE /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CE592D1F655C1C00380CEE /* Comment.swift */; }; + D7CE59301F658CA900380CEE /* IssueCommentCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D7CE592F1F658CA900380CEE /* IssueCommentCell.xib */; }; + D7ED99D41F6D71ED00E4B903 /* CreateIssueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ED99D31F6D71ED00E4B903 /* CreateIssueViewController.swift */; }; + D7ED99D61F6E100F00E4B903 /* ReposViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ED99D51F6E100F00E4B903 /* ReposViewController.swift */; }; + DC2DDE695A94509B0524CD3F /* Pods_GithubIssuesUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C987F46EEBF6AB7C71701BE6 /* Pods_GithubIssuesUITests.framework */; }; + F3AD9071E2E92DE33C2D8987 /* Pods_GithubIssuesTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C0D2DC2642FE096CC2AE419 /* Pods_GithubIssuesTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -34,9 +64,27 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 0C0D2DC2642FE096CC2AE419 /* Pods_GithubIssuesTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GithubIssuesTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3D78869E0B7334E9DAE20592 /* Pods-GithubIssuesTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GithubIssuesTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-GithubIssuesTests/Pods-GithubIssuesTests.release.xcconfig"; sourceTree = ""; }; + 492D35EB46E1B4B1FE6B6DEB /* Pods-GithubIssuesUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GithubIssuesUITests.release.xcconfig"; path = "Pods/Target Support Files/Pods-GithubIssuesUITests/Pods-GithubIssuesUITests.release.xcconfig"; sourceTree = ""; }; + 68D57E3F4AB118717745ACCC /* Pods-GithubIssuesTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GithubIssuesTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GithubIssuesTests/Pods-GithubIssuesTests.debug.xcconfig"; sourceTree = ""; }; + 9D6A46598E99AC07BF1C6DE5 /* Pods-GithubIssues.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GithubIssues.release.xcconfig"; path = "Pods/Target Support Files/Pods-GithubIssues/Pods-GithubIssues.release.xcconfig"; sourceTree = ""; }; + A999D5F615EE2A6E92606DFD /* Pods-GithubIssues.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GithubIssues.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GithubIssues/Pods-GithubIssues.debug.xcconfig"; sourceTree = ""; }; + AE8C82EDEECF19666BE564C8 /* Pods_GithubIssues.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GithubIssues.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B1F6DF003FFB6DA72643444B /* Pods-GithubIssuesUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GithubIssuesUITests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GithubIssuesUITests/Pods-GithubIssuesUITests.debug.xcconfig"; sourceTree = ""; }; + C987F46EEBF6AB7C71701BE6 /* Pods_GithubIssuesUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GithubIssuesUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D7014DC51F6814A000F0F7F7 /* IssueDetailHeaderCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueDetailHeaderCell.swift; sourceTree = ""; }; + D7014DC71F6814ED00F0F7F7 /* UIColor+extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+extension.swift"; sourceTree = ""; }; + D71E7FD01F7FADF800FD5DA0 /* BitbucketRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitbucketRouter.swift; sourceTree = ""; }; + D71E7FD21F7FAE4C00FD5DA0 /* GithubAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubAPI.swift; sourceTree = ""; }; + D71E7FD41F7FAE6100FD5DA0 /* BitbucketAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitbucketAPI.swift; sourceTree = ""; }; + D7412BD21F701F4A00E63DF6 /* IssueCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = IssueCell.xib; sourceTree = ""; }; + D776DCC41F76286300E61284 /* LoadMoreCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LoadMoreCell.xib; sourceTree = ""; }; + D776DD031F77E3D900E61284 /* IssueDetailHeaderCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = IssueDetailHeaderCell.xib; sourceTree = ""; }; + D7CB7A341F74A3C700816E33 /* ListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewController.swift; sourceTree = ""; }; + D7CB7A361F74A41900816E33 /* LoadMoreCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreCell.swift; sourceTree = ""; }; D7CE58DA1F64118400380CEE /* GithubIssues.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GithubIssues.app; sourceTree = BUILT_PRODUCTS_DIR; }; D7CE58DD1F64118400380CEE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - D7CE58DF1F64118400380CEE /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; D7CE58E21F64118400380CEE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; D7CE58E41F64118400380CEE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D7CE58E71F64118400380CEE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -47,6 +95,24 @@ D7CE58F91F64118400380CEE /* GithubIssuesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GithubIssuesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D7CE58FD1F64118400380CEE /* GithubIssuesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubIssuesUITests.swift; sourceTree = ""; }; D7CE58FF1F64118400380CEE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D7CE590B1F64127C00380CEE /* GithubRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GithubRouter.swift; sourceTree = ""; }; + D7CE590F1F64CB6F00380CEE /* DataRequest+extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DataRequest+extension.swift"; sourceTree = ""; }; + D7CE59121F64CBD800380CEE /* API.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; + D7CE59141F64CC0B00380CEE /* Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Model.swift; sourceTree = ""; }; + D7CE59171F64CC6B00380CEE /* Issue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Issue.swift; sourceTree = ""; }; + D7CE59191F64CFD500380CEE /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + D7CE591C1F64D82E00380CEE /* IssuesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssuesViewController.swift; sourceTree = ""; }; + D7CE591E1F64D99300380CEE /* LoginViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; + D7CE59201F64DBB800380CEE /* GlobalState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalState.swift; sourceTree = ""; }; + D7CE59231F65005D00380CEE /* RepoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepoViewController.swift; sourceTree = ""; }; + D7CE59251F65077B00380CEE /* IssueCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueCell.swift; sourceTree = ""; }; + D7CE59271F65209A00380CEE /* LayoutAttributable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutAttributable.swift; sourceTree = ""; }; + D7CE59291F6546D200380CEE /* IssueDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueDetailViewController.swift; sourceTree = ""; }; + D7CE592B1F655AF600380CEE /* IssueCommentCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueCommentCell.swift; sourceTree = ""; }; + D7CE592D1F655C1C00380CEE /* Comment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; + D7CE592F1F658CA900380CEE /* IssueCommentCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = IssueCommentCell.xib; sourceTree = ""; }; + D7ED99D31F6D71ED00E4B903 /* CreateIssueViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateIssueViewController.swift; sourceTree = ""; }; + D7ED99D51F6E100F00E4B903 /* ReposViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReposViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -54,6 +120,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + C9911DA0C2D7555BCE11A163 /* Pods_GithubIssues.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -61,6 +128,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F3AD9071E2E92DE33C2D8987 /* Pods_GithubIssuesTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -68,12 +136,36 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DC2DDE695A94509B0524CD3F /* Pods_GithubIssuesUITests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4664875EB8A21371C4BAF803 /* Frameworks */ = { + isa = PBXGroup; + children = ( + AE8C82EDEECF19666BE564C8 /* Pods_GithubIssues.framework */, + 0C0D2DC2642FE096CC2AE419 /* Pods_GithubIssuesTests.framework */, + C987F46EEBF6AB7C71701BE6 /* Pods_GithubIssuesUITests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 5331741D128BA823DDB27D05 /* Pods */ = { + isa = PBXGroup; + children = ( + A999D5F615EE2A6E92606DFD /* Pods-GithubIssues.debug.xcconfig */, + 9D6A46598E99AC07BF1C6DE5 /* Pods-GithubIssues.release.xcconfig */, + 68D57E3F4AB118717745ACCC /* Pods-GithubIssuesTests.debug.xcconfig */, + 3D78869E0B7334E9DAE20592 /* Pods-GithubIssuesTests.release.xcconfig */, + B1F6DF003FFB6DA72643444B /* Pods-GithubIssuesUITests.debug.xcconfig */, + 492D35EB46E1B4B1FE6B6DEB /* Pods-GithubIssuesUITests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; D7CE58D11F64118400380CEE = { isa = PBXGroup; children = ( @@ -81,6 +173,8 @@ D7CE58F11F64118400380CEE /* GithubIssuesTests */, D7CE58FC1F64118400380CEE /* GithubIssuesUITests */, D7CE58DB1F64118400380CEE /* Products */, + 5331741D128BA823DDB27D05 /* Pods */, + 4664875EB8A21371C4BAF803 /* Frameworks */, ); sourceTree = ""; }; @@ -97,9 +191,11 @@ D7CE58DC1F64118400380CEE /* GithubIssues */ = { isa = PBXGroup; children = ( - D7CE58DD1F64118400380CEE /* AppDelegate.swift */, - D7CE58DF1F64118400380CEE /* ViewController.swift */, D7CE58E11F64118400380CEE /* Main.storyboard */, + D7CE591B1F64D80F00380CEE /* View */, + D7CE59161F64CC1B00380CEE /* Model */, + D7CE590D1F64CB4C00380CEE /* Util */, + D7CE58DD1F64118400380CEE /* AppDelegate.swift */, D7CE58E41F64118400380CEE /* Assets.xcassets */, D7CE58E61F64118400380CEE /* LaunchScreen.storyboard */, D7CE58E91F64118400380CEE /* Info.plist */, @@ -125,6 +221,71 @@ path = GithubIssuesUITests; sourceTree = ""; }; + D7CE590D1F64CB4C00380CEE /* Util */ = { + isa = PBXGroup; + children = ( + D7CE59111F64CBCB00380CEE /* API */, + D7CE590E1F64CB5400380CEE /* Extension */, + D7CE59201F64DBB800380CEE /* GlobalState.swift */, + D7CE59271F65209A00380CEE /* LayoutAttributable.swift */, + ); + name = Util; + sourceTree = ""; + }; + D7CE590E1F64CB5400380CEE /* Extension */ = { + isa = PBXGroup; + children = ( + D7CE590F1F64CB6F00380CEE /* DataRequest+extension.swift */, + D7014DC71F6814ED00F0F7F7 /* UIColor+extension.swift */, + ); + name = Extension; + sourceTree = ""; + }; + D7CE59111F64CBCB00380CEE /* API */ = { + isa = PBXGroup; + children = ( + D7CE59121F64CBD800380CEE /* API.swift */, + D71E7FD21F7FAE4C00FD5DA0 /* GithubAPI.swift */, + D7CE590B1F64127C00380CEE /* GithubRouter.swift */, + D71E7FD41F7FAE6100FD5DA0 /* BitbucketAPI.swift */, + D71E7FD01F7FADF800FD5DA0 /* BitbucketRouter.swift */, + ); + name = API; + sourceTree = ""; + }; + D7CE59161F64CC1B00380CEE /* Model */ = { + isa = PBXGroup; + children = ( + D7CE59141F64CC0B00380CEE /* Model.swift */, + D7CE59171F64CC6B00380CEE /* Issue.swift */, + D7CE59191F64CFD500380CEE /* User.swift */, + D7CE592D1F655C1C00380CEE /* Comment.swift */, + ); + name = Model; + sourceTree = ""; + }; + D7CE591B1F64D80F00380CEE /* View */ = { + isa = PBXGroup; + children = ( + D7CB7A341F74A3C700816E33 /* ListViewController.swift */, + D7CE591C1F64D82E00380CEE /* IssuesViewController.swift */, + D7CE591E1F64D99300380CEE /* LoginViewController.swift */, + D7CE59231F65005D00380CEE /* RepoViewController.swift */, + D7CE59291F6546D200380CEE /* IssueDetailViewController.swift */, + D7CE592B1F655AF600380CEE /* IssueCommentCell.swift */, + D7CE592F1F658CA900380CEE /* IssueCommentCell.xib */, + D7014DC51F6814A000F0F7F7 /* IssueDetailHeaderCell.swift */, + D776DD031F77E3D900E61284 /* IssueDetailHeaderCell.xib */, + D7ED99D31F6D71ED00E4B903 /* CreateIssueViewController.swift */, + D7ED99D51F6E100F00E4B903 /* ReposViewController.swift */, + D7CE59251F65077B00380CEE /* IssueCell.swift */, + D7412BD21F701F4A00E63DF6 /* IssueCell.xib */, + D7CB7A361F74A41900816E33 /* LoadMoreCell.swift */, + D776DCC41F76286300E61284 /* LoadMoreCell.xib */, + ); + name = View; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -132,9 +293,13 @@ isa = PBXNativeTarget; buildConfigurationList = D7CE59021F64118400380CEE /* Build configuration list for PBXNativeTarget "GithubIssues" */; buildPhases = ( + 6C91CFE4185593F1C5EE423E /* [CP] Check Pods Manifest.lock */, + D7D7832A1F81243800B247DD /* ShellScript */, D7CE58D61F64118400380CEE /* Sources */, D7CE58D71F64118400380CEE /* Frameworks */, D7CE58D81F64118400380CEE /* Resources */, + 9B358046D0AF5ED9679EE94B /* [CP] Embed Pods Frameworks */, + 1C3E336DA6D59BAECEB6B9F0 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -149,9 +314,12 @@ isa = PBXNativeTarget; buildConfigurationList = D7CE59051F64118400380CEE /* Build configuration list for PBXNativeTarget "GithubIssuesTests" */; buildPhases = ( + BAAE6777FF179A310317CFED /* [CP] Check Pods Manifest.lock */, D7CE58EA1F64118400380CEE /* Sources */, D7CE58EB1F64118400380CEE /* Frameworks */, D7CE58EC1F64118400380CEE /* Resources */, + E920720D5AEA69AE4B6C5C08 /* [CP] Embed Pods Frameworks */, + BB4AD020B55BABD834A7F956 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -167,9 +335,12 @@ isa = PBXNativeTarget; buildConfigurationList = D7CE59081F64118400380CEE /* Build configuration list for PBXNativeTarget "GithubIssuesUITests" */; buildPhases = ( + F8EAD61254A56272E54236CA /* [CP] Check Pods Manifest.lock */, D7CE58F51F64118400380CEE /* Sources */, D7CE58F61F64118400380CEE /* Frameworks */, D7CE58F71F64118400380CEE /* Resources */, + 46076F4EAFB76DABA7E7789E /* [CP] Embed Pods Frameworks */, + FDD3DCF6031B043F0E918500 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -188,7 +359,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0830; - LastUpgradeCheck = 0830; + LastUpgradeCheck = 0900; ORGANIZATIONNAME = intmain; TargetAttributes = { D7CE58D91F64118400380CEE = { @@ -236,6 +407,10 @@ buildActionMask = 2147483647; files = ( D7CE58E81F64118400380CEE /* LaunchScreen.storyboard in Resources */, + D776DD041F77E3D900E61284 /* IssueDetailHeaderCell.xib in Resources */, + D776DCC51F76286300E61284 /* LoadMoreCell.xib in Resources */, + D7412BD31F701F4A00E63DF6 /* IssueCell.xib in Resources */, + D7CE59301F658CA900380CEE /* IssueCommentCell.xib in Resources */, D7CE58E51F64118400380CEE /* Assets.xcassets in Resources */, D7CE58E31F64118400380CEE /* Main.storyboard in Resources */, ); @@ -257,13 +432,205 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 1C3E336DA6D59BAECEB6B9F0 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-GithubIssues/Pods-GithubIssues-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 46076F4EAFB76DABA7E7789E /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-GithubIssuesUITests/Pods-GithubIssuesUITests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 6C91CFE4185593F1C5EE423E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-GithubIssues-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9B358046D0AF5ED9679EE94B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-GithubIssues/Pods-GithubIssues-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", + "${BUILT_PRODUCTS_DIR}/AlamofireImage/AlamofireImage.framework", + "${BUILT_PRODUCTS_DIR}/OAuthSwift/OAuthSwift.framework", + "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AlamofireImage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OAuthSwift.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyJSON.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-GithubIssues/Pods-GithubIssues-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + BAAE6777FF179A310317CFED /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-GithubIssuesTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + BB4AD020B55BABD834A7F956 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-GithubIssuesTests/Pods-GithubIssuesTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + D7D7832A1F81243800B247DD /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "${PODS_ROOT}/SwiftLint/swiftlint"; + }; + E920720D5AEA69AE4B6C5C08 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-GithubIssuesTests/Pods-GithubIssuesTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + F8EAD61254A56272E54236CA /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-GithubIssuesUITests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + FDD3DCF6031B043F0E918500 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-GithubIssuesUITests/Pods-GithubIssuesUITests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ D7CE58D61F64118400380CEE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D7CE58E01F64118400380CEE /* ViewController.swift in Sources */, + D7CE59151F64CC0B00380CEE /* Model.swift in Sources */, + D7CE592E1F655C1C00380CEE /* Comment.swift in Sources */, + D7014DC61F6814A000F0F7F7 /* IssueDetailHeaderCell.swift in Sources */, + D7CE59181F64CC6B00380CEE /* Issue.swift in Sources */, + D7CE59281F65209A00380CEE /* LayoutAttributable.swift in Sources */, + D7ED99D61F6E100F00E4B903 /* ReposViewController.swift in Sources */, + D71E7FD51F7FAE6100FD5DA0 /* BitbucketAPI.swift in Sources */, + D7014DC81F6814ED00F0F7F7 /* UIColor+extension.swift in Sources */, + D7CE59101F64CB6F00380CEE /* DataRequest+extension.swift in Sources */, + D7CB7A351F74A3C700816E33 /* ListViewController.swift in Sources */, + D7CE592A1F6546D200380CEE /* IssueDetailViewController.swift in Sources */, + D7CE591D1F64D82E00380CEE /* IssuesViewController.swift in Sources */, + D7CE590C1F64127C00380CEE /* GithubRouter.swift in Sources */, + D7CE591A1F64CFD500380CEE /* User.swift in Sources */, + D71E7FD11F7FADF800FD5DA0 /* BitbucketRouter.swift in Sources */, + D7CE59211F64DBB800380CEE /* GlobalState.swift in Sources */, + D7CE59241F65005D00380CEE /* RepoViewController.swift in Sources */, D7CE58DE1F64118400380CEE /* AppDelegate.swift in Sources */, + D7CE59261F65077B00380CEE /* IssueCell.swift in Sources */, + D7CE592C1F655AF600380CEE /* IssueCommentCell.swift in Sources */, + D7CB7A371F74A41900816E33 /* LoadMoreCell.swift in Sources */, + D71E7FD31F7FAE4C00FD5DA0 /* GithubAPI.swift in Sources */, + D7CE591F1F64D99300380CEE /* LoginViewController.swift in Sources */, + D7ED99D41F6D71ED00E4B903 /* CreateIssueViewController.swift in Sources */, + D7CE59131F64CBD800380CEE /* API.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -328,7 +695,9 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -336,7 +705,11 @@ CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -379,7 +752,9 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -387,7 +762,11 @@ CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -415,32 +794,39 @@ }; D7CE59031F64118400380CEE /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = A999D5F615EE2A6E92606DFD /* Pods-GithubIssues.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = CX2K28Z3E9; INFOPLIST_FILE = GithubIssues/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.iosDeveloper.GithubIssues; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 3.0; }; name = Debug; }; D7CE59041F64118400380CEE /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 9D6A46598E99AC07BF1C6DE5 /* Pods-GithubIssues.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = CX2K28Z3E9; INFOPLIST_FILE = GithubIssues/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.iosDeveloper.GithubIssues; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 3.0; }; name = Release; }; D7CE59061F64118400380CEE /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 68D57E3F4AB118717745ACCC /* Pods-GithubIssuesTests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -456,6 +842,7 @@ }; D7CE59071F64118400380CEE /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 3D78869E0B7334E9DAE20592 /* Pods-GithubIssuesTests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -471,6 +858,7 @@ }; D7CE59091F64118400380CEE /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = B1F6DF003FFB6DA72643444B /* Pods-GithubIssuesUITests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; DEVELOPMENT_TEAM = CX2K28Z3E9; @@ -485,6 +873,7 @@ }; D7CE590A1F64118400380CEE /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 492D35EB46E1B4B1FE6B6DEB /* Pods-GithubIssuesUITests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; DEVELOPMENT_TEAM = CX2K28Z3E9; @@ -516,6 +905,7 @@ D7CE59041F64118400380CEE /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; D7CE59051F64118400380CEE /* Build configuration list for PBXNativeTarget "GithubIssuesTests" */ = { isa = XCConfigurationList; @@ -524,6 +914,7 @@ D7CE59071F64118400380CEE /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; D7CE59081F64118400380CEE /* Build configuration list for PBXNativeTarget "GithubIssuesUITests" */ = { isa = XCConfigurationList; @@ -532,6 +923,7 @@ D7CE590A1F64118400380CEE /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; diff --git a/GithubIssues.xcodeproj/xcuserdata/leonard.xcuserdatad/xcschemes/GithubIssues.xcscheme b/GithubIssues.xcodeproj/xcuserdata/leonard.xcuserdatad/xcschemes/GithubIssues.xcscheme index 015fcc0..26c80eb 100644 --- a/GithubIssues.xcodeproj/xcuserdata/leonard.xcuserdatad/xcschemes/GithubIssues.xcscheme +++ b/GithubIssues.xcodeproj/xcuserdata/leonard.xcuserdatad/xcschemes/GithubIssues.xcscheme @@ -1,18 +1,64 @@ + + + + + + + + + + + + + + + + + + @@ -20,12 +66,23 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> + + + + @@ -35,6 +92,16 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES"> + + + + diff --git a/GithubIssues.xcworkspace/contents.xcworkspacedata b/GithubIssues.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..d02e29b --- /dev/null +++ b/GithubIssues.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/GithubIssues.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/GithubIssues.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/GithubIssues.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/GithubIssues/API.swift b/GithubIssues/API.swift new file mode 100644 index 0000000..6faabbe --- /dev/null +++ b/GithubIssues/API.swift @@ -0,0 +1,35 @@ +// +// API.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 10.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import Foundation +import Alamofire +import SwiftyJSON + +struct App { + static var api: API = { + switch GlobalState.instance.serviceType { + case .github: + return GithubAPI() + case .bitbucket: + return BitbucketAPI() + } + }() +} + +protocol API { + typealias IssueResponsesHandler = (DataResponse<[Model.Issue]>) -> Void + typealias CommentResponsesHandler = (DataResponse<[Model.Comment]>) -> Void + func getToekn(handler: @escaping (() -> Void)) + func tokenRefresh(handler: @escaping (() -> Void)) + func repoIssues(owner: String, repo: String) -> (Int, @escaping IssueResponsesHandler) -> Void + func issueComment(owner: String, repo: String, number: Int) -> (Int, @escaping CommentResponsesHandler) -> Void + func createComment(owner: String, repo: String, number: Int, comment: String, completionHandler: @escaping (DataResponse) -> Void ) + func createIssue(owner: String, repo: String, title: String, body: String, completionHandler: @escaping (DataResponse) -> Void ) + func closeIssue(owner: String, repo: String, number: Int, issue: Model.Issue, completionHandler: @escaping (DataResponse) -> Void) + func openIssue(owner: String, repo: String, number: Int, issue: Model.Issue, completionHandler: @escaping (DataResponse) -> Void) +} diff --git a/GithubIssues/AppDelegate.swift b/GithubIssues/AppDelegate.swift index 0a4b26c..d79ec41 100644 --- a/GithubIssues/AppDelegate.swift +++ b/GithubIssues/AppDelegate.swift @@ -7,40 +7,49 @@ // import UIKit +import OAuthSwift @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. + if !GlobalState.instance.isLoggedIn { + let loginViewController = LoginViewController.viewController + DispatchQueue.main.asyncAfter(deadline: .now() + 0.0, execute: { [weak self] in + self?.window?.rootViewController?.present(loginViewController, animated: false, completion: nil) + }) + } return true } func applicationWillResignActive(_ application: UIApplication) { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } func applicationDidEnterBackground(_ application: UIApplication) { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } func applicationWillEnterForeground(_ application: UIApplication) { - // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } func applicationDidBecomeActive(_ application: UIApplication) { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - } + } + func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { + print("url: \(url.absoluteString)") + if (url.host == "oauth-callback") { + OAuthSwift.handle(url: url) + } + return true + } } - diff --git a/GithubIssues/Assets.xcassets/AppIcon.appiconset/Contents.json b/GithubIssues/Assets.xcassets/AppIcon.appiconset/Contents.json index 36d2c80..d8db8d6 100644 --- a/GithubIssues/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/GithubIssues/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,5 +1,15 @@ { "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, { "idiom" : "iphone", "size" : "29x29", @@ -30,6 +40,16 @@ "size" : "60x60", "scale" : "3x" }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, { "idiom" : "ipad", "size" : "29x29", @@ -59,6 +79,16 @@ "idiom" : "ipad", "size" : "76x76", "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" } ], "info" : { diff --git a/GithubIssues/Assets.xcassets/Contents.json b/GithubIssues/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/GithubIssues/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/GithubIssues/Assets.xcassets/Octocat.imageset/Contents.json b/GithubIssues/Assets.xcassets/Octocat.imageset/Contents.json new file mode 100644 index 0000000..d037f78 --- /dev/null +++ b/GithubIssues/Assets.xcassets/Octocat.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Octocat.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/GithubIssues/Assets.xcassets/Octocat.imageset/Octocat.png b/GithubIssues/Assets.xcassets/Octocat.imageset/Octocat.png new file mode 100644 index 0000000..91057da Binary files /dev/null and b/GithubIssues/Assets.xcassets/Octocat.imageset/Octocat.png differ diff --git a/GithubIssues/Assets.xcassets/bitbucket.imageset/Contents.json b/GithubIssues/Assets.xcassets/bitbucket.imageset/Contents.json new file mode 100644 index 0000000..0573ff8 --- /dev/null +++ b/GithubIssues/Assets.xcassets/bitbucket.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "bitbucket_512.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/GithubIssues/Assets.xcassets/bitbucket.imageset/bitbucket_512.png b/GithubIssues/Assets.xcassets/bitbucket.imageset/bitbucket_512.png new file mode 100644 index 0000000..7440fbd Binary files /dev/null and b/GithubIssues/Assets.xcassets/bitbucket.imageset/bitbucket_512.png differ diff --git a/GithubIssues/Assets.xcassets/ic_comment.imageset/Contents.json b/GithubIssues/Assets.xcassets/ic_comment.imageset/Contents.json new file mode 100644 index 0000000..26c322d --- /dev/null +++ b/GithubIssues/Assets.xcassets/ic_comment.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_comment.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_comment@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_comment@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/GithubIssues/Assets.xcassets/ic_comment.imageset/ic_comment.png b/GithubIssues/Assets.xcassets/ic_comment.imageset/ic_comment.png new file mode 100644 index 0000000..f4f2060 Binary files /dev/null and b/GithubIssues/Assets.xcassets/ic_comment.imageset/ic_comment.png differ diff --git a/GithubIssues/Assets.xcassets/ic_comment.imageset/ic_comment@2x.png b/GithubIssues/Assets.xcassets/ic_comment.imageset/ic_comment@2x.png new file mode 100644 index 0000000..15803da Binary files /dev/null and b/GithubIssues/Assets.xcassets/ic_comment.imageset/ic_comment@2x.png differ diff --git a/GithubIssues/Assets.xcassets/ic_comment.imageset/ic_comment@3x.png b/GithubIssues/Assets.xcassets/ic_comment.imageset/ic_comment@3x.png new file mode 100644 index 0000000..81e75fc Binary files /dev/null and b/GithubIssues/Assets.xcassets/ic_comment.imageset/ic_comment@3x.png differ diff --git a/GithubIssues/Assets.xcassets/ic_issues_closed.imageset/Contents.json b/GithubIssues/Assets.xcassets/ic_issues_closed.imageset/Contents.json new file mode 100644 index 0000000..a1fabf5 --- /dev/null +++ b/GithubIssues/Assets.xcassets/ic_issues_closed.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_issues_closed.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_issues_closed@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_issues_closed@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/GithubIssues/Assets.xcassets/ic_issues_closed.imageset/ic_issues_closed.png b/GithubIssues/Assets.xcassets/ic_issues_closed.imageset/ic_issues_closed.png new file mode 100644 index 0000000..b1f2507 Binary files /dev/null and b/GithubIssues/Assets.xcassets/ic_issues_closed.imageset/ic_issues_closed.png differ diff --git a/GithubIssues/Assets.xcassets/ic_issues_closed.imageset/ic_issues_closed@2x.png b/GithubIssues/Assets.xcassets/ic_issues_closed.imageset/ic_issues_closed@2x.png new file mode 100644 index 0000000..ec8bdde Binary files /dev/null and b/GithubIssues/Assets.xcassets/ic_issues_closed.imageset/ic_issues_closed@2x.png differ diff --git a/GithubIssues/Assets.xcassets/ic_issues_closed.imageset/ic_issues_closed@3x.png b/GithubIssues/Assets.xcassets/ic_issues_closed.imageset/ic_issues_closed@3x.png new file mode 100644 index 0000000..4d10a06 Binary files /dev/null and b/GithubIssues/Assets.xcassets/ic_issues_closed.imageset/ic_issues_closed@3x.png differ diff --git a/GithubIssues/Assets.xcassets/ic_issues_open.imageset/Contents.json b/GithubIssues/Assets.xcassets/ic_issues_open.imageset/Contents.json new file mode 100644 index 0000000..8bdae60 --- /dev/null +++ b/GithubIssues/Assets.xcassets/ic_issues_open.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_issues_open.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_issues_open@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_issues_open@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/GithubIssues/Assets.xcassets/ic_issues_open.imageset/ic_issues_open.png b/GithubIssues/Assets.xcassets/ic_issues_open.imageset/ic_issues_open.png new file mode 100644 index 0000000..f39c315 Binary files /dev/null and b/GithubIssues/Assets.xcassets/ic_issues_open.imageset/ic_issues_open.png differ diff --git a/GithubIssues/Assets.xcassets/ic_issues_open.imageset/ic_issues_open@2x.png b/GithubIssues/Assets.xcassets/ic_issues_open.imageset/ic_issues_open@2x.png new file mode 100644 index 0000000..d2cf046 Binary files /dev/null and b/GithubIssues/Assets.xcassets/ic_issues_open.imageset/ic_issues_open@2x.png differ diff --git a/GithubIssues/Assets.xcassets/ic_issues_open.imageset/ic_issues_open@3x.png b/GithubIssues/Assets.xcassets/ic_issues_open.imageset/ic_issues_open@3x.png new file mode 100644 index 0000000..86634ee Binary files /dev/null and b/GithubIssues/Assets.xcassets/ic_issues_open.imageset/ic_issues_open@3x.png differ diff --git a/GithubIssues/Base.lproj/Main.storyboard b/GithubIssues/Base.lproj/Main.storyboard index 273375f..f21e9a0 100644 --- a/GithubIssues/Base.lproj/Main.storyboard +++ b/GithubIssues/Base.lproj/Main.storyboard @@ -1,26 +1,529 @@ - - + + + + + - + + + + + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + diff --git a/GithubIssues/BitbucketAPI.swift b/GithubIssues/BitbucketAPI.swift new file mode 100644 index 0000000..695f608 --- /dev/null +++ b/GithubIssues/BitbucketAPI.swift @@ -0,0 +1,199 @@ +// BitbucketAPI.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 30.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import Foundation +import Alamofire +import SwiftyJSON +import OAuthSwift + +struct BitbucketAPI: API { + let bitbucketOAuth: OAuth2Swift = OAuth2Swift( + consumerKey: "vx2MD5uVaRyLgMxype", + consumerSecret: "CA9cZxqWEgRDpZCCYy353WG763J8McWH", + authorizeUrl: "https://bitbucket.org/site/oauth2/authorize", + accessTokenUrl: "https://bitbucket.org/site/oauth2/access_token", + responseType: "code" + ) + func getOauthKey(user: String, password: String, completionHandler: @escaping (DataResponse) -> Void) { + var headers: HTTPHeaders = [:] + if let authorizationHeader = Request.authorizationHeader(user: user, password: password) { + headers[authorizationHeader.key] = authorizationHeader.value + } + let parameters: Parameters = ["client_secret": BitbucketRouter.clientSecret , "scopes": ["public_repo"], "note": "admin script" ] + Alamofire.request(BitbucketRouter.authKey(parameters, headers)) + .responseSwiftyJSON { json in + print(json) + completionHandler(json) + } + } + func getToekn(handler: @escaping (() -> Void)) { + guard let url = URL(string:"ISSAPP://oauth-callback/bitbucket") else { return } + bitbucketOAuth.authorize( + withCallbackURL: url, + scope: "issue:write", state:"state", + success: {(credential, _, _) in + GlobalState.instance.token = credential.oauthToken + GlobalState.instance.refreshToken = credential.oauthRefreshToken + GlobalState.instance.serviceType = .bitbucket + App.api = BitbucketAPI() + handler() + }, failure: { ( error ) in + print(error.localizedDescription) + }) + } + func tokenRefresh(handler: @escaping (() -> Void)) { + guard let refreshToken = GlobalState.instance.refreshToken else { return } + bitbucketOAuth.renewAccessToken( + withRefreshToken: refreshToken, + success: { (credential, _, _) in + GlobalState.instance.token = credential.oauthToken + GlobalState.instance.refreshToken = credential.oauthRefreshToken + GlobalState.instance.serviceType = .bitbucket + App.api = BitbucketAPI() + handler() + }, failure: { (error) in + print(error.localizedDescription) + }) + } + func repoIssues(owner: String, repo: String) -> (Int, @escaping IssueResponsesHandler) -> Void { + return { (page: Int, handler: @escaping IssueResponsesHandler) in + let parameters: Parameters = ["page": page, "state": "all"] + Alamofire.request(BitbucketRouter.repoIssues(owner: owner, repo: repo, parameters: parameters)).responseSwiftyJSON { (dataResponse: DataResponse) in + if dataResponse.response?.statusCode == 401 { + var retryCount = 1 + self.tokenRefresh { + if retryCount > 1 { + return + } + retryCount += 1 + self.repoIssues(owner: owner, repo: repo)(page, handler) + } + return + } + let result = dataResponse.map({ (json: JSON) -> [Model.Issue] in + return json["values"].arrayValue.map { (json: JSON) -> Model.Issue in + Model.Issue(json: json.githubIssueToBitbucket) + } + }) + handler(result) + } + } + } + func issueComment(owner: String, repo: String, number: Int) -> (Int, @escaping CommentResponsesHandler) -> Void { + return { page, handler in + let parameters: Parameters = ["page": page] + Alamofire.request(BitbucketRouter.issueDetail(owner: owner, repo: repo, number: number, parameters: parameters)).responseSwiftyJSON { (dataResponse: DataResponse) in + let result = dataResponse.map({ (json: JSON) -> [Model.Comment] in + return json["values"].arrayValue.map { + Model.Comment(json: $0.githubCommentToBitbucket) + } + }) + handler(result) + } + } + } + func createComment(owner: String, repo: String, number: Int, comment: String, completionHandler: @escaping (DataResponse) -> Void ) { + let parameters: Parameters = ["body": comment] + Alamofire.request(BitbucketRouter.createComment(owner: owner, repo: repo, number: number, parameters: parameters)).responseSwiftyJSON { (dataResponse: DataResponse) in + let result = dataResponse.map({ (json: JSON) -> Model.Comment in + Model.Comment(json: json) + }) + completionHandler(result) + } + } + func createIssue(owner: String, repo: String, title: String, body: String, completionHandler: @escaping (DataResponse) -> Void ) { + let parameters: Parameters = ["title": title, "content": ["raw":body]] + Alamofire.request(BitbucketRouter.createIssue(owner: owner, repo: repo, parameters: parameters)).responseSwiftyJSON { (dataResponse: DataResponse) in + let result = dataResponse.map({ (json: JSON) -> Model.Issue in + Model.Issue(json: json.githubIssueToBitbucket) + }) + completionHandler(result) + } + } + func closeIssue(owner: String, repo: String, number: Int, issue: Model.Issue, completionHandler: @escaping (DataResponse) -> Void) { + var dict = issue.toDict + dict["state"] = Model.Issue.State.closed.display + Alamofire.request(BitbucketRouter.editIssue(owner: owner, repo: repo, number: number, parameters: dict)).responseSwiftyJSON { (dataResponse: DataResponse) in + let result = dataResponse.map({ (json: JSON) -> Model.Issue in + Model.Issue(json: json) + }) + completionHandler(result) + } + } + func openIssue(owner: String, repo: String, number: Int, issue: Model.Issue, completionHandler: @escaping (DataResponse) -> Void) { + var dict = issue.toDict + dict["state"] = Model.Issue.State.open.display + Alamofire.request(BitbucketRouter.editIssue(owner: owner, repo: repo, number: number, parameters: dict)).responseSwiftyJSON { (dataResponse: DataResponse) in + let result = dataResponse.map({ (json: JSON) -> Model.Issue in + Model.Issue(json: json) + }) + completionHandler(result) + } + } + +} + +extension JSON { + /* + + id -> id + number -> id + title -> title + comments -> + body -> [content][raw] + createdAt -> created_on + closedAt -> state, new -> open, closed -> closed + + */ + var githubIssueToBitbucket: JSON { + var json = JSON() + json["id"] = self["id"] + json["number"] = self["id"] + json["title"] = self["title"] + json["body"] = self["content"]["raw"] + json["user"] = self["reporter"].githubUserToBitbucket + switch self["state"].stringValue { + case "new": + json["state"].string = "open" + case "closed": + json["state"].string = "closed" + default: + json["state"].string = "none" + } + let created_at = (self["created_on"].stringValue.components(separatedBy: ".").first ?? "")+"Z" + json["created_at"].string = created_at + return json + } + /*id -> uuid + login -> username + avatar_url -> ["links"]["avatar"]["href"]*/ + var githubUserToBitbucket: JSON { + var json = JSON() + json["id"] = self["uuid"] + json["login"] = self["username"] + json["avatar_url"] = self["links"]["avatar"]["href"] + return json + } + /* + id -> id + user -> user.git + body -> ["content"]["raw"] + createdAt -> created_on + updatedAt -> updated_on + */ + var githubCommentToBitbucket: JSON { + var json = JSON() + json["id"] = self["id"] + json["user"] = self["user"].githubUserToBitbucket + json["body"] = self["content"]["raw"] + let createdAt = (self["created_on"].stringValue.components(separatedBy: ".").first ?? "")+"Z" + json["created_at"].string = createdAt + let updatedAt = (self["updated_at"].stringValue.components(separatedBy: ".").first ?? "")+"Z" + json["updated_at"].string = updatedAt + return json + } +} diff --git a/GithubIssues/BitbucketRouter.swift b/GithubIssues/BitbucketRouter.swift new file mode 100644 index 0000000..109dea9 --- /dev/null +++ b/GithubIssues/BitbucketRouter.swift @@ -0,0 +1,83 @@ +// +// BitbucketRouter.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 30.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import Foundation +import Alamofire +import SwiftyJSON + +enum BitbucketRouter { + case authKey(Parameters, HTTPHeaders) + case repoIssues(owner: String, repo: String, parameters: Parameters) + case issueDetail(owner: String, repo: String, number: Int, parameters: Parameters) + case createComment(owner: String, repo: String, number: Int, parameters: Parameters) + case createIssue(owner: String, repo: String, parameters: Parameters) + case editIssue(owner: String, repo: String, number: Int, parameters: Parameters) +} + +extension BitbucketRouter: URLRequestConvertible { + static let baseURLString: String = "https://api.bitbucket.org" + static let clientID: String = "36c48adc3d1433fbd286" + static let clientSecret: String = "a911bfd178a79f25d14c858a1199cd76d9e92f3b" + var method: HTTPMethod { + switch self { + case .authKey: + return .put + case .repoIssues, + .issueDetail + : + return .get + case .createComment, + .createIssue + : + return .post + case .editIssue + : + return .patch + } + } + var path: String { + switch self { + case .authKey: + return "/authorizations/clients/\(BitbucketRouter.clientID)/\(Date().timeIntervalSince1970)" + case let .repoIssues(owner, repo, _): + return "/2.0/repositories/\(owner)/\(repo)/issues" + case let .issueDetail(owner, repo, number, _): + return "/2.0/repositories/\(owner)/\(repo)/issues/\(number)/comments" + case let .createComment(owner, repo, number, _): + return "/2.0/repositories/\(owner)/\(repo)/issues/\(number)/comments" + case let .createIssue(owner, repo, _): + return "/2.0/repositories/\(owner)/\(repo)/issues" + case let .editIssue(owner, repo, number, _): + return "/repos/\(owner)/\(repo)/issues/\(number)" + } + } + func asURLRequest() throws -> URLRequest { + let url = try BitbucketRouter.baseURLString.asURL() + var urlRequest = URLRequest(url: url.appendingPathComponent(path)) + urlRequest.httpMethod = method.rawValue + if let token = GlobalState.instance.token, !token.isEmpty { + urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } + switch self { + case let .authKey(parameters, headers): + headers.forEach { (key, value) in urlRequest.addValue(value, forHTTPHeaderField: key) } + urlRequest = try JSONEncoding.default.encode(urlRequest, with: parameters) + case let .repoIssues(_, _, parameters): + urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters) + case let .issueDetail(_, _, _, parameters): + urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters) + case let .createComment(_, _, _, parameters): + urlRequest = try JSONEncoding.default.encode(urlRequest, with: parameters) + case let .createIssue(_, _, parameters): + urlRequest = try JSONEncoding.default.encode(urlRequest, with: parameters) + case let .editIssue(_, _, _, parameters): + urlRequest = try JSONEncoding.default.encode(urlRequest, with: parameters) + } + return urlRequest + } +} diff --git a/GithubIssues/Comment.swift b/GithubIssues/Comment.swift new file mode 100644 index 0000000..2192a6e --- /dev/null +++ b/GithubIssues/Comment.swift @@ -0,0 +1,29 @@ +// +// Comment.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 10.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import Foundation +import SwiftyJSON + +extension Model { + public struct Comment { + let id: Int + let user: Model.User + let body: String + let createdAt: Date? + let updatedAt: Date? + public init(json: JSON) { + id = json["id"].intValue + user = Model.User(json: json["user"]) + body = json["body"].stringValue + let format = DateFormatter() + format.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + createdAt = format.date(from: json["created_at"].stringValue) + updatedAt = format.date(from: json["updated_at"].stringValue) + } + } +} diff --git a/GithubIssues/CreateIssueViewController.swift b/GithubIssues/CreateIssueViewController.swift new file mode 100644 index 0000000..5a1355b --- /dev/null +++ b/GithubIssues/CreateIssueViewController.swift @@ -0,0 +1,51 @@ +// +// CreateIssueViewController.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 16.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import UIKit +import Alamofire + +class CreateIssueViewController: UIViewController { + @IBOutlet var textView: UITextView! + @IBOutlet var titleTextField: UITextField! + var createdIssue: Model.Issue? + var owner: String = "" + var repo: String = "" + override func viewDidLoad() { + super.viewDidLoad() + setup() + } +} + +extension CreateIssueViewController { + func setup() { + textView.layer.borderColor = UIColor(red:0.80, green:0.80, blue:0.80, alpha:1.00).cgColor + textView.layer.borderWidth = 1.0 / UIScreen.main.scale + textView.layer.cornerRadius = 5 + } + func uploadIssue() { + let title = titleTextField.text ?? "" + let body = textView.text ?? "" + App.api.createIssue(owner: owner, repo: repo, title: title, body: body) { [weak self] (dataResponse: DataResponse) in + guard let `self` = self else { return } + switch dataResponse.result { + case .success(let issue): + print(issue) + self.createdIssue = issue + self.performSegue(withIdentifier: "UnwindToIssues", sender: self) + case .failure: + break + } + } + } +} + +extension CreateIssueViewController { + @IBAction func doneButtonTapped(_ sender: Any) { + uploadIssue() + } +} diff --git a/GithubIssues/DataRequest+extension.swift b/GithubIssues/DataRequest+extension.swift new file mode 100644 index 0000000..c836bf3 --- /dev/null +++ b/GithubIssues/DataRequest+extension.swift @@ -0,0 +1,52 @@ +// +// DataRequest+extension.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 10.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import Foundation +import Alamofire +import SwiftyJSON + +enum BackendError: Error { + case network(error: Error) + case dataSerialization(reason: String) + case jsonSerialization(error: Error) + case objectSerialization(reason: String) + case xmlSerialization(error: Error) +} + +extension DataRequest { + @discardableResult + public func responseSwiftyJSON(_ completionHandler: @escaping (DataResponse) -> Void) -> Self { + let responseSerializer = DataResponseSerializer { request, response, data, error in + guard error == nil else { + DataRequest.errorMessage(response, error: error, data: data) + return .failure(error!) + } + let result = DataRequest + .jsonResponseSerializer(options: .allowFragments) + .serializeResponse(request, response, data, error) + switch result { + case .success(let value): + if let _ = response { + return .success(JSON(value)) + } else { + let failureReason = "JSON could not be serialized into response object: \(value)" + let error = BackendError.objectSerialization(reason: failureReason) + DataRequest.errorMessage(response, error: error, data: data) + return .failure(error) + } + case .failure(let error): + DataRequest.errorMessage(response, error: error, data: data) + return .failure(error) + } + } + return response(responseSerializer: responseSerializer, completionHandler: completionHandler) + } + static func errorMessage(_ response: HTTPURLResponse?, error: Error?, data: Data?) { + debugPrint("status: \(response?.statusCode ?? -1), error message:\(error.debugDescription)") + } +} diff --git a/GithubIssues/GithubAPI.swift b/GithubIssues/GithubAPI.swift new file mode 100644 index 0000000..a560be8 --- /dev/null +++ b/GithubIssues/GithubAPI.swift @@ -0,0 +1,132 @@ +// +// GithubAPI.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 30.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import Foundation +import Alamofire +import SwiftyJSON +import OAuthSwift + +struct GithubAPI: API { + let githubOAuth: OAuth2Swift = OAuth2Swift( + consumerKey: "36c48adc3d1433fbd286", + consumerSecret: "a911bfd178a79f25d14c858a1199cd76d9e92f3b", + authorizeUrl: "https://github.com/login/oauth/authorize", + accessTokenUrl: "https://github.com/login/oauth/access_token", + responseType: "code" + ) + + func getOauthKey(user: String, password: String, completionHandler: @escaping (DataResponse) -> Void) { + var headers: HTTPHeaders = [:] + if let authorizationHeader = Request.authorizationHeader(user: user, password: password) { + headers[authorizationHeader.key] = authorizationHeader.value + } + let parameters: Parameters = ["client_secret": GithubRouter.clientSecret , "scopes": ["public_repo"], "note": "admin script" ] + Alamofire.request(GithubRouter.authKey(parameters, headers)) + .responseSwiftyJSON { json in + print(json) + completionHandler(json) + } + } + func getToekn(handler: @escaping (() -> Void)) { + githubOAuth.authorize( + withCallbackURL: URL(string: "ISSAPP://oauth-callback/github")!, + scope: "user,repo", state:"state", + success: { credential, _, _ in + GlobalState.instance.token = credential.oauthToken + GlobalState.instance.serviceType = .github + App.api = GithubAPI() + handler() + }, + failure: { error in + print(error.localizedDescription) + }) + } + func tokenRefresh(handler: @escaping (() -> Void)) { + guard let refreshToken = GlobalState.instance.refreshToken else { return } + githubOAuth.renewAccessToken(withRefreshToken: refreshToken, success: { (credential, _, _) in + GlobalState.instance.token = credential.oauthToken + GlobalState.instance.serviceType = .github + App.api = GithubAPI() + handler() + }, failure: { (error) in + print(error.localizedDescription) + }) + } + + func repoIssues(owner: String, repo: String) -> (Int, @escaping IssueResponsesHandler) -> Void { + return { (page: Int, handler: @escaping IssueResponsesHandler) in + let parameters: Parameters = ["page": page, "state": "all"] + Alamofire.request(GithubRouter.repoIssues(owner: owner, repo: repo, parameters: parameters)).responseSwiftyJSON { (dataResponse: DataResponse) in + let result = dataResponse.map({ (json: JSON) -> [Model.Issue] in + return json.arrayValue.map { + Model.Issue(json: $0) + } + }) + handler(result) + } + } + } + + func issueComment(owner: String, repo: String, number: Int) -> (Int, @escaping CommentResponsesHandler) -> Void { + return { page, handler in + let parameters: Parameters = ["page": page] + Alamofire.request(GithubRouter.issueDetail(owner: owner, repo: repo, number: number, parameters: parameters)).responseSwiftyJSON { (dataResponse: DataResponse) in + let result = dataResponse.map({ (json: JSON) -> [Model.Comment] in + return json.arrayValue.map { + Model.Comment(json: $0) + } + }) + handler(result) + } + } + } + + func createComment(owner: String, repo: String, number: Int, comment: String, completionHandler: @escaping (DataResponse) -> Void ) { + let parameters: Parameters = ["body": comment] + Alamofire.request(GithubRouter.createComment(owner: owner, repo: repo, number: number, parameters: parameters)).responseSwiftyJSON { (dataResponse: DataResponse) in + let result = dataResponse.map({ (json: JSON) -> Model.Comment in + Model.Comment(json: json) + }) + completionHandler(result) + } + } + + func createIssue(owner: String, repo: String, title: String, body: String, completionHandler: @escaping (DataResponse) -> Void ) { + let parameters: Parameters = ["title": title, "body": body] + Alamofire.request(GithubRouter.createIssue(owner: owner, repo: repo, parameters: parameters)).responseSwiftyJSON { (dataResponse: DataResponse) in + let result = dataResponse.map({ (json: JSON) -> Model.Issue in + Model.Issue(json: json) + }) + completionHandler(result) + } + } + + func closeIssue(owner: String, repo: String, number: Int, issue: Model.Issue, completionHandler: @escaping (DataResponse) -> Void) { + var dict = issue.toDict + dict["state"] = Model.Issue.State.closed.display + Alamofire.request(GithubRouter.editIssue(owner: owner, repo: repo, number: number, parameters: dict)).responseSwiftyJSON { (dataResponse: DataResponse) in + let result = dataResponse.map({ (json: JSON) -> Model.Issue in + Model.Issue(json: json) + }) + completionHandler(result) + } + + } + + func openIssue(owner: String, repo: String, number: Int, issue: Model.Issue, completionHandler: @escaping (DataResponse) -> Void) { + var dict = issue.toDict + dict["state"] = Model.Issue.State.open.display + Alamofire.request(GithubRouter.editIssue(owner: owner, repo: repo, number: number, parameters: dict)).responseSwiftyJSON { (dataResponse: DataResponse) in + let result = dataResponse.map({ (json: JSON) -> Model.Issue in + Model.Issue(json: json) + }) + completionHandler(result) + } + + } +} diff --git a/GithubIssues/GithubRouter.swift b/GithubIssues/GithubRouter.swift new file mode 100644 index 0000000..0b7b98f --- /dev/null +++ b/GithubIssues/GithubRouter.swift @@ -0,0 +1,88 @@ +// +// Router.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 9.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import Foundation +import Alamofire +import SwiftyJSON + +enum GithubRouter { + case authKey(Parameters, HTTPHeaders) + case repoIssues(owner: String, repo: String, parameters: Parameters) + case issueDetail(owner: String, repo: String, number: Int, parameters: Parameters) + case createComment(owner: String, repo: String, number: Int, parameters: Parameters) + case createIssue(owner: String, repo: String, parameters: Parameters) + case editIssue(owner: String, repo: String, number: Int, parameters: Parameters) +} + +extension GithubRouter: URLRequestConvertible { + static let baseURLString: String = "https://api.github.com" + static let clientID: String = "36c48adc3d1433fbd286" + static let clientSecret: String = "a911bfd178a79f25d14c858a1199cd76d9e92f3b" + + var method: HTTPMethod { + switch self { + case .authKey: + return .put + case .repoIssues, + .issueDetail + : + return .get + case .createComment, + .createIssue + : + return .post + case .editIssue + : + return .patch + } + } + + var path: String { + switch self { + case .authKey: + return "/authorizations/clients/\(GithubRouter.clientID)/\(Date().timeIntervalSince1970)" + case let .repoIssues(owner, repo, _): + return "/repos/\(owner)/\(repo)/issues" + case let .issueDetail(owner, repo, number, _): + return "/repos/\(owner)/\(repo)/issues/\(number)/comments" + case let .createComment(owner, repo, number, _): + return "/repos/\(owner)/\(repo)/issues/\(number)/comments" + case let .createIssue(owner, repo, _): + return "/repos/\(owner)/\(repo)/issues" + case let .editIssue(owner, repo, number, _): + return "/repos/\(owner)/\(repo)/issues/\(number)" + } + } + func asURLRequest() throws -> URLRequest { + let url = try GithubRouter.baseURLString.asURL() + + var urlRequest = URLRequest(url: url.appendingPathComponent(path)) + urlRequest.httpMethod = method.rawValue + if let token = GlobalState.instance.token, !token.isEmpty { + urlRequest.setValue("token \(token)", forHTTPHeaderField: "Authorization") + } + + switch self { + case let .authKey(parameters, headers): + headers.forEach { (key, value) in urlRequest.addValue(value, forHTTPHeaderField: key) } + urlRequest = try JSONEncoding.default.encode(urlRequest, with: parameters) + case let .repoIssues(_, _, parameters): + urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters) + case let .issueDetail(_, _, _, parameters): + urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters) + case let .createComment(_, _, _, parameters): + urlRequest = try JSONEncoding.default.encode(urlRequest, with: parameters) + case let .createIssue(_, _, parameters): + urlRequest = try JSONEncoding.default.encode(urlRequest, with: parameters) + case let .editIssue(_, _, _, parameters): + urlRequest = try JSONEncoding.default.encode(urlRequest, with: parameters) + } + + return urlRequest + } +} diff --git a/GithubIssues/GlobalState.swift b/GithubIssues/GlobalState.swift new file mode 100644 index 0000000..3d35c14 --- /dev/null +++ b/GithubIssues/GlobalState.swift @@ -0,0 +1,95 @@ +// +// GlobalState.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 10.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import Foundation + +final class GlobalState { + static let instance = GlobalState() + enum ServiceType: String { + case github + case bitbucket + } + struct Constants { + static let tokenKey = "token" + static let ownerKey = "owner" + static let repoKey = "repo" + static let reposKey = "repos" + static let serviceType = "serviceType" + static let refreshTokenKey = "refreshToken" + } + var token: String? { + get { + let token = UserDefaults.standard.string(forKey: Constants.tokenKey) + return token + } + set { + UserDefaults.standard.set(newValue, forKey: Constants.tokenKey) + } + } + var refreshToken: String? { + get { + let token = UserDefaults.standard.string(forKey: Constants.refreshTokenKey) + return token + } + set { + UserDefaults.standard.set(newValue, forKey: Constants.refreshTokenKey) + } + } + var owner: String { + get { + let owner = UserDefaults.standard.string(forKey: Constants.ownerKey) ?? "" + return owner + } + set { + UserDefaults.standard.set(newValue, forKey: Constants.ownerKey) + } + } + var repo: String { + get { + let repo = UserDefaults.standard.string(forKey: Constants.repoKey) ?? "" + return repo + } + set { + UserDefaults.standard.set(newValue, forKey: Constants.repoKey) + } + } + + var serviceType: ServiceType { + get { + let type = UserDefaults.standard.string(forKey: Constants.serviceType) ?? "" + let serviceType = ServiceType(rawValue: type) ?? ServiceType.github + return serviceType + } + set { + UserDefaults.standard.set(newValue.rawValue, forKey: Constants.serviceType) + } + } + + var isLoggedIn: Bool { + let isEmpty = token?.isEmpty ?? true + return !isEmpty + } + + func addRepo(owner:String, repo: String) { + let dict = ["owner": owner, "repo" : repo] + var repos: [[String: String]] = (UserDefaults.standard.array(forKey: Constants.reposKey) as? [[String : String]]) ?? [] + repos.append(dict) + + UserDefaults.standard.set(NSSet(array: repos).allObjects, forKey: Constants.reposKey) + } + + var repos: [(owner: String, repo:String)] { + let repoDicts: [[String: String]] = (UserDefaults.standard.array(forKey: Constants.reposKey) as? [[String : String]]) ?? [] + let repos = repoDicts.map { (repoDict: [String: String]) -> (String, String) in + let owner = repoDict["owner"] ?? "" + let repo = repoDict["repo"] ?? "" + return (owner, repo) + } + return repos + } +} diff --git a/GithubIssues/Info.plist b/GithubIssues/Info.plist index d052473..ee44cc0 100644 --- a/GithubIssues/Info.plist +++ b/GithubIssues/Info.plist @@ -16,6 +16,17 @@ APPL CFBundleShortVersionString 1.0 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + ISSAPP + + + CFBundleVersion 1 LSRequiresIPhoneOS @@ -28,6 +39,8 @@ armv7 + UIStatusBarStyle + UIStatusBarStyleLightContent UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -41,5 +54,7 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIViewControllerBasedStatusBarAppearance + diff --git a/GithubIssues/Issue.swift b/GithubIssues/Issue.swift new file mode 100644 index 0000000..947a808 --- /dev/null +++ b/GithubIssues/Issue.swift @@ -0,0 +1,96 @@ +// +// Issue.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 10.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import Foundation +import SwiftyJSON + +extension Model { + public struct Issue { + let id: Int + let number: Int + let title: String + let user: Model.User + let state: State + let comments: Int + let body: String + let createdAt: Date? + let updatedAt: Date? + let closedAt: Date? + + public init(json: JSON) { + print("issue json: \(json)") + id = json["id"].intValue + number = json["number"].intValue + title = json["title"].stringValue + user = Model.User(json: json["user"]) + state = State(rawValue: json["state"].stringValue) ?? .none + comments = json["comments"].intValue + body = json["body"].stringValue + + let format = DateFormatter() + format.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + createdAt = format.date(from: json["created_at"].stringValue) + updatedAt = format.date(from: json["updated_at"].stringValue) + closedAt = format.date(from: json["closed_at"].stringValue) + } + } +} + +extension Model.Issue { + var toDict: [String: Any] { + let format = DateFormatter() + format.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + var dict: [String : Any] = [ + "id": id, + "number": number, + "title": title, + "comments": comments, + "body": body, + "state": state.display, + "user": [ + "id": user.id, + "login": user.login, + "acatar_url": (user.avatarURL?.absoluteString ?? "")] + ] + if let createdAt = createdAt { + dict["createdAt"] = format.string(from: createdAt) + } + if let updatedAt = updatedAt { + dict["updatedAt"] = format.string(from: updatedAt) + } + if let closedAt = closedAt { + dict["closedAt"] = format.string(from: closedAt) + } + + print("dict: \(dict)") + return dict + + } +} + +extension Model.Issue { + enum State: String { + case open + case closed + case none + + var display: String { + switch self { + case .open: return "opened" + case .closed: return "closed" + case .none: return "-" + } + } + } +} + +extension Model.Issue: Equatable { + public static func == (lhs: Model.Issue, rhs: Model.Issue) -> Bool { + return lhs.id == rhs.id + } +} diff --git a/GithubIssues/IssueCell.swift b/GithubIssues/IssueCell.swift new file mode 100644 index 0000000..49f9e4d --- /dev/null +++ b/GithubIssues/IssueCell.swift @@ -0,0 +1,54 @@ +// +// IssueCell.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 10.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import UIKit + +protocol CellProtocol { + associatedtype Item + func update(data: Item) + static var cellFromNib: Self { get } +} + +final class IssueCell: UICollectionViewCell { + + @IBOutlet var stateButton: UIButton! + @IBOutlet var titleLabel: UILabel! + @IBOutlet var contentLabel: UILabel! + @IBOutlet var commentCountButton: UIButton! +} + +extension IssueCell: CellProtocol { + typealias Item = Model.Issue + + static var cellFromNib: IssueCell { + guard let cell = Bundle.main.loadNibNamed("IssueCell", owner: nil, options: nil)?.first as? IssueCell else { + return IssueCell() + } + return cell + } + + func update(data issue: Model.Issue) { + titleLabel.text = issue.title + contentLabel.text = issue.body + let createdAt = issue.createdAt?.string(dateFormat: "dd MMM yyyy") ?? "-" + contentLabel.text = "#\(issue.number) \(issue.state.display) on \(createdAt) by \(issue.user.login)" + commentCountButton.setTitle("\(issue.comments)", for: .normal) + stateButton.isSelected = issue.state == .closed + let commentCountHidden: Bool = issue.comments == 0 + commentCountButton.isHidden = commentCountHidden + } +} + +extension Date { + func string(dateFormat: String, locale: String = "en-US") -> String { + let format = DateFormatter() + format.dateFormat = dateFormat + format.locale = Locale(identifier: locale) + return format.string(from: self) + } +} diff --git a/GithubIssues/IssueCell.xib b/GithubIssues/IssueCell.xib new file mode 100644 index 0000000..089a7a6 --- /dev/null +++ b/GithubIssues/IssueCell.xib @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GithubIssues/IssueCommentCell.swift b/GithubIssues/IssueCommentCell.swift new file mode 100644 index 0000000..cc60aab --- /dev/null +++ b/GithubIssues/IssueCommentCell.swift @@ -0,0 +1,50 @@ +// +// IssueCommentCell.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 10.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import UIKit +import AlamofireImage + +final class IssueCommentCell: UICollectionViewCell, CellProtocol { + + @IBOutlet var bodyLabel: UILabel! + @IBOutlet var titleLabel: UILabel! + @IBOutlet var profileImageView: UIImageView! + @IBOutlet var commentContanerView: UIView! + + override func awakeFromNib() { + profileImageView.layer.cornerRadius = profileImageView.frame.size.width / 2 + commentContanerView.layer.borderWidth = 1 + commentContanerView.layer.borderColor = UIColor.groupTableViewBackground.cgColor + } + +} + +extension IssueCommentCell { + func update(data: Model.Comment) { + update(data: data, withImage: true) + } + + typealias Item = Model.Comment + + func update(data comment: Model.Comment, withImage: Bool = true) { + if let url = comment.user.avatarURL { + profileImageView.af_setImage(withURL: url) + } + + let createdAt = comment.createdAt?.string(dateFormat: "dd MMM yyyy") ?? "-" + titleLabel.text = "\(comment.user.login) commented on \(createdAt)" + bodyLabel.text = comment.body + } + + static var cellFromNib: IssueCommentCell { + guard let cell = Bundle.main.loadNibNamed("IssueCommentCell", owner: nil, options: nil)?.first as? IssueCommentCell else { + return IssueCommentCell() + } + return cell + } +} diff --git a/GithubIssues/IssueCommentCell.xib b/GithubIssues/IssueCommentCell.xib new file mode 100644 index 0000000..fe5a73a --- /dev/null +++ b/GithubIssues/IssueCommentCell.xib @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GithubIssues/IssueDetailHeaderCell.swift b/GithubIssues/IssueDetailHeaderCell.swift new file mode 100644 index 0000000..3410890 --- /dev/null +++ b/GithubIssues/IssueDetailHeaderCell.swift @@ -0,0 +1,111 @@ +// +// IssueDetailHeaderView.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 12.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import UIKit + +@IBDesignable +class IssueDetailHeaderCell: UICollectionReusableView { + + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var stateButton: UIButton! + @IBOutlet weak var infoLabel: UILabel! + + @IBOutlet weak var commentContainerView: UIView! + @IBOutlet weak var avatarImageView: UIImageView! + @IBOutlet weak var commentInfoLabel: UILabel! + @IBOutlet weak var commentBodyLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + setup() + print("awakeFromNib") + } + + public func loadNib() -> UIView { + let bundle = Bundle(for: type(of: self)) + let nib = UINib(nibName: "IssueDetailHeaderCell", bundle: bundle) + return nib.instantiate(withOwner: self, options: nil)[0] as! UIView // swiftlint:disable:this force_cast + } + + override public init(frame: CGRect) { + super.init(frame: frame) + self.setupNib() + } + + // MARK: - NSCoding + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.setupNib() + } + + fileprivate func setupNib() { + let view = self.loadNib() + + self.addSubview(view) + view.translatesAutoresizingMaskIntoConstraints = false + let bindings = ["view": view] + self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[view]|", options:[], metrics:nil, views: bindings)) + self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view]|", options:[], metrics:nil, views: bindings)) + } + + static let estimateSizeCell: IssueDetailHeaderCell = IssueDetailHeaderCell() +} + +// MARK: - setup +extension IssueDetailHeaderCell { + func setup() { + stateButton.clipsToBounds = true + stateButton.layer.cornerRadius = 2 + + stateButton.setTitle(Model.Issue.State.open.display, for: .normal) + stateButton.setBackgroundImage(UIColor.opened.toImage(), for: .normal) + stateButton.setTitle(Model.Issue.State.closed.display, for: .selected) + stateButton.setBackgroundImage(UIColor.closed.toImage(), for: .selected) + + avatarImageView.clipsToBounds = true + avatarImageView.layer.cornerRadius = avatarImageView.bounds.midX + + commentContainerView.clipsToBounds = true + commentContainerView.layer.cornerRadius = 2 + commentContainerView.layer.borderColor = UIColor.groupTableViewBackground.cgColor + commentContainerView.layer.borderWidth = 1 + + } + static func headerSize(issue: Model.Issue, width: CGFloat) -> CGSize { + + IssueDetailHeaderCell.estimateSizeCell.update(data: issue) + let targetSize = CGSize(width: width, height: 0) + let size = IssueDetailHeaderCell.estimateSizeCell.systemLayoutSizeFitting( + targetSize, + withHorizontalFittingPriority: UILayoutPriorityRequired, + verticalFittingPriority: UILayoutPriorityDefaultLow + ) + let width = size.width == 0 ? IssueDetailHeaderCell.estimateSizeCell.bounds.width : size.width + let height = size.height == 0 ? IssueDetailHeaderCell.estimateSizeCell.bounds.height : size.height + let cellSize = CGSize(width: width, height: height) + return cellSize + } +} + +extension IssueDetailHeaderCell { + func update(data: Model.Issue, withImage: Bool = true) { + + let createdAt = data.createdAt?.string(dateFormat: "dd MMM yyyy") ?? "-" + titleLabel.text = data.title + stateButton.isSelected = data.state == .closed + infoLabel.text = "\(data.user.login) \(data.state.display) this issue on \(createdAt) · \(data.comments) comments" + + //body + if let url = data.user.avatarURL, withImage { + avatarImageView.af_setImage(withURL: url) + } + commentInfoLabel.text = "\(data.user.login) commented on \(createdAt)" + commentBodyLabel.text = data.body + } + +} diff --git a/GithubIssues/IssueDetailHeaderCell.xib b/GithubIssues/IssueDetailHeaderCell.xib new file mode 100644 index 0000000..8f4fa8a --- /dev/null +++ b/GithubIssues/IssueDetailHeaderCell.xib @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GithubIssues/IssueDetailViewController.swift b/GithubIssues/IssueDetailViewController.swift new file mode 100644 index 0000000..1f31385 --- /dev/null +++ b/GithubIssues/IssueDetailViewController.swift @@ -0,0 +1,172 @@ +// +// IssueDetailViewController.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 10.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import UIKit +import Alamofire + +class IssueDetailViewController: ListViewController { + + @IBOutlet var commentInputBottomConstraint: NSLayoutConstraint! + + @IBOutlet var commentTextField: UITextField! + var owner: String = "" + var repo: String = "" + var issue: Model.Issue! + var headerSize: CGSize = CGSize.zero + + override func viewDidLoad() { + super.viewDidLoad() + api = App.api.issueComment(owner: owner, repo: repo, number: issue.number) + setup() + + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + addKeyboardNotification() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + removeKeyboardNOtification() + } + + override func setup() { + super.setup() + var inset = self.collectionView.contentInset + inset.bottom = 46 + self.collectionView.contentInset = inset + collectionView.register(UINib(nibName: "IssueCommentCell", bundle: nil), forCellWithReuseIdentifier: "IssueCommentCell") + title = "#\(issue!.number)" + } + + @IBAction func sendButtonTapped(_ sender: Any) { + let comment = commentTextField.text ?? "" + App.api.createComment(owner: owner, repo: repo, number: issue.number, comment: comment) { [weak self] (dataResponse: DataResponse) in + guard let `self` = self else { return } + switch dataResponse.result { + case .success(let comment): + self.addComment(comment: comment) + self.commentTextField.text = "" + self.commentTextField.resignFirstResponder() + + break + case .failure: + break + } + } + } + + @IBAction func stateButtonTapped(_ sender: Any) { + chagneState() + } + + override func cellIdentifier() -> String { + return "IssueCommentCell" + } + + override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + switch kind { + + case UICollectionElementKindSectionHeader: + + let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "HeaderCell", for: indexPath) as? IssueDetailHeaderCell ?? IssueDetailHeaderCell() + + headerView.update(data: issue) + return headerView + + case UICollectionElementKindSectionFooter: + let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "LoadMoreCell", for: indexPath) as? LoadMoreCell ?? LoadMoreCell() + + loadMoreCell = footerView + return footerView + + default: + + assert(false, "Unexpected element kind") + } + } + + override func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { + + if headerSize == CGSize.zero { + headerSize = IssueDetailHeaderCell.headerSize(issue: issue, width: collectionView.frame.width) + + } + return headerSize + } +} + +extension IssueDetailViewController { + + func addKeyboardNotification() { + NotificationCenter.default.addObserver(forName: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil, queue: nil) { [weak self] (notifiaction: Notification) in + guard let `self` = self else { return } + guard let keyboardBounds = notifiaction.userInfo?[UIKeyboardFrameEndUserInfoKey] as? CGRect else { return } + guard let animationDuration = notifiaction.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? TimeInterval else { return } + guard let animationCurve = notifiaction.userInfo?[UIKeyboardAnimationCurveUserInfoKey] as? UInt else { return } + let animationOptions = UIViewAnimationOptions(rawValue: animationCurve) + let keyboardHeight = keyboardBounds.height + let inputBottom = self.view.frame.height - keyboardBounds.origin.y + print("inputBottom: \(inputBottom)") + print("keyboard: \(keyboardHeight)") + var inset = self.collectionView.contentInset + inset.bottom = inputBottom + 46 + self.collectionView.contentInset = inset + self.commentInputBottomConstraint.constant = inputBottom + UIView.animate(withDuration: animationDuration, delay: 0, options: animationOptions, animations: { + self.view.layoutIfNeeded() + }, completion: nil) + } + } + + func removeKeyboardNOtification() { + NotificationCenter.default.removeObserver(self) + } +} + +extension IssueDetailViewController { + +} + +extension IssueDetailViewController { + func addComment(comment: Model.Comment) { + let newIndexPath = IndexPath(item: datasource.count, section: 0) + datasource.append(comment) + collectionView.insertItems(at: [newIndexPath]) + + collectionView.scrollToItem(at: newIndexPath, at: .bottom, animated: true) + + } + + func chagneState() { + switch issue.state { + case .open, .none: + App.api.closeIssue(owner: owner, repo: repo, number: issue.number, issue: issue, completionHandler: { [weak self] (dataResponse: DataResponse) in + switch dataResponse.result { + case .success(let issue): + print("issue: \(issue)") + self?.issue = issue + case .failure(let error): + print(error) + } + + }) + case .closed: + App.api.openIssue(owner: owner, repo: repo, number: issue.number, issue: issue, completionHandler: { [weak self] (dataResponse: DataResponse) in + switch dataResponse.result { + case .success(let issue): + print("issue: \(issue)") + self?.issue = issue + case .failure(let error): + print(error) + } + }) + } + } +} diff --git a/GithubIssues/IssuesViewController.swift b/GithubIssues/IssuesViewController.swift new file mode 100644 index 0000000..1fc177d --- /dev/null +++ b/GithubIssues/IssuesViewController.swift @@ -0,0 +1,76 @@ +// +// IssuesViewController.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 10.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import UIKit +import Alamofire + +final class IssuesViewController: ListViewController { + var owner: String = "" + var repo: String = "" + + override func viewDidLoad() { + super.viewDidLoad() + api = App.api.repoIssues(owner: owner, repo: repo) + collectionView.delegate = self + collectionView.dataSource = self + setup() + + } + + override func setup() { + super.setup() + collectionView.register(UINib(nibName: "IssueCell", bundle: nil), forCellWithReuseIdentifier: "IssueCell") + + } + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if let detailViewController = segue.destination as? IssueDetailViewController,let issue = sender as? Model.Issue { + detailViewController.issue = issue + detailViewController.repo = repo + detailViewController.owner = owner + } else if let navigationController = segue.destination as? UINavigationController, let createIssueViewController = navigationController.topViewController as? CreateIssueViewController { + createIssueViewController.repo = repo + createIssueViewController.owner = owner + } + } + + @IBAction func unwindFromCreate(_ segue: UIStoryboardSegue) { + if let createViewController = segue.source as? CreateIssueViewController, let createdIssue = createViewController.createdIssue { + datasource.insert(createdIssue, at: 0) + DispatchQueue.main.async { [weak self] in + self?.collectionView.reloadData() + } + + } + } + + override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let data = datasource[indexPath.item] + self.performSegue(withIdentifier: "PushIssueDetail", sender: data) + } + + override func cellIdentifier() -> String { + return "IssueCell" + } + + override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + switch kind { + + case UICollectionElementKindSectionHeader: + assert(false, "Unexpected element kind") + + case UICollectionElementKindSectionFooter: + let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "LoadMoreCell", for: indexPath) as? LoadMoreCell ?? LoadMoreCell() + loadMoreCell = footerView + return footerView + + default: + + assert(false, "Unexpected element kind") + } + } +} diff --git a/GithubIssues/LayoutAttributable.swift b/GithubIssues/LayoutAttributable.swift new file mode 100644 index 0000000..250e5df --- /dev/null +++ b/GithubIssues/LayoutAttributable.swift @@ -0,0 +1,42 @@ +// +// LayoutAttributable.swift +// TaewanArchitectureStudy +// +// Created by taewan on 2017. 2. 11.. +// Copyright © 2017년 taewankim. All rights reserved. +// + +import UIKit + +protocol LayoutEstimatable: class { + static var estimatedLayout: [IndexPath: CGSize] { get set } + static func estimatedSizeReset(indexPath: IndexPath?) +} + +extension LayoutEstimatable where Self: UICollectionViewCell { + static func estimatedSizeReset(indexPath: IndexPath? = nil) { + if let key = indexPath { + estimatedLayout[key] = nil + } else { + estimatedLayout = [:] + } + } + + func estimateLayoutAttributes(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { + if layoutAttributes.isHidden { + return layoutAttributes + } + + if let layoutSize = Self.estimatedLayout[layoutAttributes.indexPath] { + layoutAttributes.size = layoutSize + } else { + layoutAttributes.size = contentView.systemLayoutSizeFitting( + layoutAttributes.size, + withHorizontalFittingPriority: UILayoutPriorityRequired, + verticalFittingPriority: UILayoutPriorityDefaultLow) + Self.estimatedLayout[layoutAttributes.indexPath] = layoutAttributes.size + } + return layoutAttributes + } + +} diff --git a/GithubIssues/ListViewController.swift b/GithubIssues/ListViewController.swift new file mode 100644 index 0000000..5a09029 --- /dev/null +++ b/GithubIssues/ListViewController.swift @@ -0,0 +1,140 @@ +// +// ListViewController.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 22.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import UIKit +import Alamofire + +protocol DatasourceRefreshable: class { + associatedtype Item + var datasource: [Item] { get set } + var needRefreshDatasource: Bool { get set } +} + +extension DatasourceRefreshable { + func setNeedRefreshDatasource() { + needRefreshDatasource = true + } + func refreshDataSourceIfNeeded() { + if needRefreshDatasource { + datasource = [] + needRefreshDatasource = false + } + } +} + +class ListViewController: UIViewController, DatasourceRefreshable, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { + typealias Item = CellType.Item + var isLoading: Bool = false + var loadMoreCell: LoadMoreCell? + @IBOutlet var collectionView: UICollectionView! + var needRefreshDatasource: Bool = false + typealias PageIndicator = Int + var datasource: [Item] = [] + let refreshControl = UIRefreshControl() + var page: Int = 1 + var canLoadMore: Bool = true + fileprivate var estimatedSizes: [IndexPath: CGSize] = [:] + fileprivate let estimateCell: CellType = CellType.cellFromNib + typealias IssueResponsesHandler = (DataResponse<[Item]>) -> Void + var api: ((Int, @escaping IssueResponsesHandler) -> Void)? + func loadMore(indexPath: IndexPath) { + guard indexPath.item == datasource.count - 1 && !isLoading && canLoadMore else { return } + load() + } + func setup() { + collectionView.refreshControl = refreshControl + collectionView.alwaysBounceVertical = true + refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged) + load() + loadMoreCell?.load() + } + func load() { + guard isLoading == false else {return } + isLoading = true + api?(page, {[weak self] (response: DataResponse<[Item]>) -> Void in + guard let `self` = self else { return } + switch response.result { + case .success(let items): + self.dataLoaded(items: items) + self.isLoading = false + case .failure: + self.isLoading = false + break + } + }) + } + func dataLoaded(items: [Item]) { + refreshDataSourceIfNeeded() + page += 1 + if items.isEmpty { + canLoadMore = false + loadMoreCell?.loadDone() + } + refreshControl.endRefreshing() + datasource.append(contentsOf: items) + collectionView.reloadData() + } + @objc func refresh() { + page = 1 + canLoadMore = true + loadMoreCell?.load() + setNeedRefreshDatasource() + load() + } + func cellIdentifier() -> String { + return "" + } + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + /* + 사이즈 + + 인덱싱된 사이즈가 있으면 리턴. + 데이터를 가져옴. + estimateCell에 데이트 업데이트함. + 가로 사이즈를 받아, 쎌 사이즈를 잼. + 가져온 사이즈를 인덱싱. + 그 사이즈를 리턴. + + */ + var estimatedSize = estimatedSizes[indexPath] ?? CGSize.zero + if estimatedSize != .zero { + return estimatedSize + } + let data = datasource[indexPath.item] + estimateCell.update(data: data) + let targetSize = CGSize(width: collectionView.frame.size.width, height: 50) + estimatedSize = estimateCell.contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityDefaultLow) + estimatedSizes[indexPath] = estimatedSize + return estimatedSize + } + func numberOfSections(in collectionView: UICollectionView) -> Int { + return 1 + } + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return datasource.count + } + func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + loadMore(indexPath: indexPath) + } + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier(), for: indexPath) as? CellType else { + return UICollectionViewCell() + } + let issue = datasource[indexPath.item] + cell.update(data: issue) + return cell + } + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + } + func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + return UICollectionReusableView() + } + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { + return CGSize.zero + } +} diff --git a/GithubIssues/LoadMoreCell.swift b/GithubIssues/LoadMoreCell.swift new file mode 100644 index 0000000..95defcf --- /dev/null +++ b/GithubIssues/LoadMoreCell.swift @@ -0,0 +1,54 @@ +// +// LoadMoreView.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 22.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import UIKit + +@IBDesignable +class LoadMoreCell: UICollectionReusableView { + @IBOutlet var activityIndicatorView: UIActivityIndicatorView! + @IBOutlet var doneView: UIView! + + public func loadNib() -> UIView { + let bundle = Bundle(for: type(of: self)) + let nib = UINib(nibName: "LoadMoreCell", bundle: bundle) + guard let view = nib.instantiate(withOwner: self, options: nil)[0] as? UIView else { return UIView() } + return view + } + + override public init(frame: CGRect) { + super.init(frame: frame) + self.setupNib() + } + + // MARK: - NSCoding + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.setupNib() + } + + fileprivate func setupNib() { + let view = self.loadNib() + self.addSubview(view) + view.translatesAutoresizingMaskIntoConstraints = false + let bindings = ["view": view] + self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[view]|", options:[], metrics:nil, views: bindings)) + self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view]|", options:[], metrics:nil, views: bindings)) + } +} + +extension LoadMoreCell { + func loadDone() { + activityIndicatorView.isHidden = true + doneView.isHidden = false + } + + func load() { + activityIndicatorView.isHidden = false + doneView.isHidden = true + } +} diff --git a/GithubIssues/LoadMoreCell.xib b/GithubIssues/LoadMoreCell.xib new file mode 100644 index 0000000..49f3ac1 --- /dev/null +++ b/GithubIssues/LoadMoreCell.xib @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GithubIssues/LoginViewController.swift b/GithubIssues/LoginViewController.swift new file mode 100644 index 0000000..e0f5b5c --- /dev/null +++ b/GithubIssues/LoginViewController.swift @@ -0,0 +1,44 @@ +// +// LoginViewController.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 10.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import UIKit +import Alamofire +import SwiftyJSON +import OAuthSwift + +class LoginViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + static var viewController: LoginViewController { + guard let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "LoginViewController") as? LoginViewController else { return LoginViewController() } + return viewController + } + @IBAction func githubLoginButtonTapped(_ sender: Any) { + App.api.getToekn { [weak self] in + self?.dismiss(animated: true, completion: { + }) + } + } + + @IBAction func bitbucketLoginButtonTapped(_ sender: Any) { + App.api.getToekn { [weak self] in + self?.dismiss(animated: true, completion: { + }) + } + } +} diff --git a/GithubIssues/Model.swift b/GithubIssues/Model.swift new file mode 100644 index 0000000..a502f70 --- /dev/null +++ b/GithubIssues/Model.swift @@ -0,0 +1,13 @@ +// +// Model.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 10.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import Foundation + +public struct Model { + +} diff --git a/GithubIssues/RepoViewController.swift b/GithubIssues/RepoViewController.swift new file mode 100644 index 0000000..5547451 --- /dev/null +++ b/GithubIssues/RepoViewController.swift @@ -0,0 +1,69 @@ +// +// RepoViewController.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 10.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import UIKit + +class RepoViewController: UIViewController { + + @IBOutlet var ownerTextField: UITextField! + @IBOutlet var repoTextField: UITextField! + + override func viewDidLoad() { + super.viewDidLoad() + + ownerTextField.text = GlobalState.instance.owner + repoTextField.text = GlobalState.instance.repo + +// repoTextField.layer.frame = CGRect(x: 100, y: 100, width: 100, height: 100) + // Do any additional setup after loading the view. + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + // MARK: - Navigation + + override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { + guard let owner = ownerTextField.text, let repo = repoTextField.text else { return false } + return !(owner.isEmpty || repo.isEmpty) + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + + if segue.identifier == "EnterRepoSegue" { + guard let owner = ownerTextField.text, let repo = repoTextField.text else { return } + GlobalState.instance.owner = owner + GlobalState.instance.repo = repo + GlobalState.instance.addRepo(owner: owner, repo: repo) + guard let issuesViewController = segue.destination as? IssuesViewController else { return } + issuesViewController.owner = owner + issuesViewController.repo = repo + } + } + + @IBAction func unwindFromRepos(_ segue: UIStoryboardSegue) { + if let reposViewController = segue.source as? ReposViewController, let (owner, repo) = reposViewController.selectedRepo { + ownerTextField.text = owner + repoTextField.text = repo + DispatchQueue.main.async { [weak self] in + self?.performSegue(withIdentifier: "EnterRepoSegue", sender: nil) + } + + } + } + + @IBAction func logoutButtonTapped(_ sender: Any?) { + GlobalState.instance.token = "" + let loginViewController = LoginViewController.viewController + DispatchQueue.main.asyncAfter(deadline: .now() + 0.0, execute: { [weak self] in + self?.present(loginViewController, animated: true, completion: nil) + }) + } +} diff --git a/GithubIssues/ReposViewController.swift b/GithubIssues/ReposViewController.swift new file mode 100644 index 0000000..e5c2a90 --- /dev/null +++ b/GithubIssues/ReposViewController.swift @@ -0,0 +1,54 @@ +// +// ReposViewController.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 17.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import UIKit + +class ReposViewController: UIViewController { + + @IBOutlet var tableView: UITableView! + var selectedRepo: (owner: String, repo: String)? + let datasource = GlobalState.instance.repos + + override func viewDidLoad() { + super.viewDidLoad() + setup() + } + + // MARK: - Navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + + } +} + +extension ReposViewController { + func setup() { + tableView.dataSource = self + tableView.delegate = self + } +} + +extension ReposViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return datasource.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "RepoCell", for: indexPath) + let data = datasource[indexPath.row] + cell.textLabel?.text = "/repo/\(data.owner)/\(data.repo)" + return cell + } +} + +extension ReposViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let data = datasource[indexPath.row] + selectedRepo = data + self.performSegue(withIdentifier: "unwindToIssue", sender: self) + } +} diff --git a/GithubIssues/UIColor+extension.swift b/GithubIssues/UIColor+extension.swift new file mode 100644 index 0000000..e79ee97 --- /dev/null +++ b/GithubIssues/UIColor+extension.swift @@ -0,0 +1,37 @@ +// +// UIColor+extension.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 12.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import UIKit + +extension UIColor { + static var opened: UIColor { + return UIColor(red: 131/255, green: 189/255, blue: 71/255, alpha: 1) + } + + static var closed: UIColor { + return UIColor(red: 176/255, green: 65/255, blue: 32/255, alpha: 1) + } + +} + +extension UIColor { + public func toImage(_ size: CGSize = CGSize(width: 1, height: 1)) -> UIImage { + let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) + UIGraphicsBeginImageContext(rect.size) + + if let context = UIGraphicsGetCurrentContext() { + context.setFillColor(self.cgColor) + context.fill(rect) + if let image = UIGraphicsGetImageFromCurrentImageContext() { + UIGraphicsEndImageContext() + return image + } + } + return UIImage() + } +} diff --git a/GithubIssues/User.swift b/GithubIssues/User.swift new file mode 100644 index 0000000..6d57a6a --- /dev/null +++ b/GithubIssues/User.swift @@ -0,0 +1,26 @@ +// +// User.swift +// GithubIssues +// +// Created by Leonard on 2017. 9. 10.. +// Copyright © 2017년 intmain. All rights reserved. +// + +import Foundation +import SwiftyJSON + +extension Model { + public struct User { + let id: String + let login: String + let avatarURL: URL? + + public init(json: JSON) { + id = json["id"].stringValue + login = json["login"].stringValue + + avatarURL = URL(string: json["avatar_url"].stringValue) + } + + } +} diff --git a/GithubIssues/ViewController.swift b/GithubIssues/ViewController.swift deleted file mode 100644 index b23b975..0000000 --- a/GithubIssues/ViewController.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// ViewController.swift -// GithubIssues -// -// Created by Leonard on 2017. 9. 9.. -// Copyright © 2017년 intmain. All rights reserved. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view, typically from a nib. - } - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } - - -} - diff --git a/Podfile b/Podfile new file mode 100644 index 0000000..3eb271a --- /dev/null +++ b/Podfile @@ -0,0 +1,22 @@ +# Uncomment the next line to define a global platform for your project + platform :ios, '10.0' + +target 'GithubIssues' do + # Comment the next line if you're not using Swift and don't want to use dynamic frameworks + use_frameworks! + pod 'Alamofire', '~> 4.5' + pod 'AlamofireImage' + pod 'SwiftyJSON' + pod 'OAuthSwift', '~> 1.1.2' + pod 'SwiftLint' + target 'GithubIssuesTests' do + inherit! :search_paths + # Pods for testing + end + + target 'GithubIssuesUITests' do + inherit! :search_paths + # Pods for testing + end + +end