diff --git a/.gitignore b/.gitignore index 7ad871c..6697aeb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +### Custom + +# About API Key +Config.swift # Created by https://www.gitignore.io/api/xcode,swift,macos # Edit at https://www.gitignore.io/?templates=xcode,swift,macos @@ -129,4 +133,4 @@ iOSInjectionProject/ /*.gcno **/xcshareddata/WorkspaceSettings.xcsettings -# End of https://www.gitignore.io/api/xcode,swift,macos \ No newline at end of file +# End of https://www.gitignore.io/api/xcode,swift,macos diff --git a/README.md b/README.md index 527ef20..13aa5b5 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,28 @@ -# TREE 🌳 -> μ „ 세계적인 이슈λ₯Ό 확인할 수 μžˆλŠ” μ• ν”Œλ¦¬μΌ€μ΄μ…˜ +# TREE +> μ „ 세계 이슈(κΈ‰μƒμŠΉ 검색어, λ‰΄μŠ€ 기사)λ₯Ό μ‹€μ‹œκ°„μœΌλ‘œ μ œκ³΅ν•˜λŠ” μ• ν”Œλ¦¬μΌ€μ΄μ…˜ -## ⚠️ GROUND RULE ⚠️ +## Team Gardener +> BoostCamp 3th A-1 Team -- κ³΅ν†΅λœ μ½”λ“œ μ»¨λ²€μ…˜μ„ μ‚¬μš©ν•©λ‹ˆλ‹€. -- μ½”λ“œμ˜ νš¨μœ¨μ„±μ„ μ¦λŒ€μ‹œν‚΅λ‹ˆλ‹€. -- λŒ€ν™”λ₯Ό ν†΅ν•œ 적극적인 ν”Όλ“œλ°±μ„ μ£Όκ³ λ°›μŠ΅λ‹ˆλ‹€. -- ν”„λ‘œμ νŠΈ 완성을 λͺ©ν‘œλ‘œ ν•©λ‹ˆλ‹€. +- [λ°•μ„±μ€€](https://github.com/godpp) +- [κΉ€ν˜œλ¦¬](https://github.com/kimhyeri) +- [κΉ€ν˜„νƒœ](https://github.com/onemoongit) -## πŸ“‘ μ•± κΈ°νšμ„œ πŸ“‘ - ### κΈ°λŠ₯ 트리 -![ν…μŠ€νŠΈλͺ©λ‘](./image/1.png) +## GROUND RULE +* κ³΅ν†΅λœ μ½”λ“œ μ»¨λ²€μ…˜μ„ μ‚¬μš©ν•©λ‹ˆλ‹€. + - [Wiki](https://github.com/boostcamp3-iOS/team-a1/wiki/Swift-Style-Guide)에 μ •μ˜ν•˜μ˜€μŠ΅λ‹ˆλ‹€. +* μ½”λ“œμ˜ νš¨μœ¨μ„±μ„ μ¦λŒ€μ‹œν‚΅λ‹ˆλ‹€. + - μž¬μ‚¬μš© κ°€λŠ₯ν•œ 뢀뢄에 λŒ€ν•΄μ„œλŠ” λͺ¨λ“ˆλ‘œ ν™œμš©ν•˜μ—¬ μ‚¬μš©ν•©λ‹ˆλ‹€. + - PR에 λŒ€ν•œ μ½”λ©˜νŠΈλ₯Ό ν™œμš©ν•˜μ—¬ μ„œλ‘œμ˜ μ½”λ“œλ₯Ό 적극적으둜 λ¦¬λ·°ν•©λ‹ˆλ‹€. +* λŒ€ν™”λ₯Ό ν†΅ν•œ 적극적인 ν”Όλ“œλ°±μ„ μ£Όκ³ λ°›μŠ΅λ‹ˆλ‹€. + - 맀일 μ•„μΉ¨ 10μ‹œλΆ€ν„° 데일리 μŠ€ν¬λŸΌμ„ 톡해 μ–΄μ œ ν•œ μž‘μ—…μ„ κ³΅μœ ν•˜κ³  였늘 ν•΄μ•Όν•  μž‘μ—…μ„ λͺ…ν™•ν•˜κ²Œ μ„€μ •ν•©λ‹ˆλ‹€. -## πŸ”œ 핡심 κΈ°λŠ₯ κ΅¬ν˜„ μˆœμ„œ πŸ”œ +## κΈ°λŠ₯ 트리 +![](./image/functionTree.png) -- 검색을 톡해 μœ μ €κ°€ μ›ν•˜λŠ” λ‹€μ–‘ν•œ 아티클 검색 κ°€λŠ₯ -- 가독성을 높이고 λΉ λ₯Έ 응닡 속도λ₯Ό λ³΄μ—¬μ£ΌλŠ” λ„€μ΄ν‹°λΈŒ λ·°μ–΄ κΈ°λŠ₯ -- μ „ μ„Έκ³„μ—μ„œ μ—…λ°μ΄νŠΈ λ˜λŠ” λ‰΄μŠ€λ₯Ό μ‹€μ‹œκ°„μœΌλ‘œ ν‘œμ‹œ -- 아티클 μŠ€ν¬λž©μ„ ν•  수 μžˆλŠ” 아카이빙 κΈ°λŠ₯ -- 파파고 APIλ₯Ό ν™œμš©ν•˜μ—¬ λ‹€μ–‘ν•œ μ–Έμ–΄λ‘œ λ²ˆμ—­ κ°€λŠ₯ +## ν˜„μž¬κΉŒμ§€ μž‘μ—…ν•œ λ·° + +> Search + +![](./image/Search.png) diff --git a/image/1.png b/image/1.png deleted file mode 100644 index da80393..0000000 Binary files a/image/1.png and /dev/null differ diff --git a/image/Search.png b/image/Search.png new file mode 100644 index 0000000..3c15aa5 Binary files /dev/null and b/image/Search.png differ diff --git a/image/functionTree.png b/image/functionTree.png new file mode 100644 index 0000000..48a11ea Binary files /dev/null and b/image/functionTree.png differ diff --git a/tree/tree.xcodeproj/project.pbxproj b/tree/tree.xcodeproj/project.pbxproj index b6b7913..c9dd37d 100644 --- a/tree/tree.xcodeproj/project.pbxproj +++ b/tree/tree.xcodeproj/project.pbxproj @@ -18,6 +18,22 @@ 35DC0DFF21F6FB3200F30416 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = 35DC0DFE21F6FB3200F30416 /* .swiftlint.yml */; }; 35DC0E0821F70BBC00F30416 /* ArticleDetail.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 35DC0E0721F70BBC00F30416 /* ArticleDetail.storyboard */; }; 45A74A72C78EC54DD12F5CA6 /* Pods_tree.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF0163F8362378789ACC30D4 /* Pods_tree.framework */; }; + 6A0957AC2208490E00D46741 /* Extension+PickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A0957AB2208490E00D46741 /* Extension+PickerView.swift */; }; + 6A0957AE220963B300D46741 /* SearchFilterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A0957AD220963B300D46741 /* SearchFilterProtocol.swift */; }; + 6A198CC8220C9F51002421AC /* ArticleTypeEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A198CC7220C9F51002421AC /* ArticleTypeEnum.swift */; }; + 6A198CCA220CAA9A002421AC /* DefaultLabelView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6A198CC9220CAA9A002421AC /* DefaultLabelView.xib */; }; + 6A198CCC220CAAB5002421AC /* DefautLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A198CCB220CAAB5002421AC /* DefautLabelView.swift */; }; + 6A47BF43220E0638008BEA7A /* LiveFeedTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A47BF41220E0638008BEA7A /* LiveFeedTableViewCell.swift */; }; + 6A47BF44220E0638008BEA7A /* LiveFeedTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6A47BF42220E0638008BEA7A /* LiveFeedTableViewCell.xib */; }; + 6A5D282A22013FA5009EC8ED /* Extension+ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5D282922013FA5009EC8ED /* Extension+ImageView.swift */; }; + 6A5D282C220187F8009EC8ED /* Extension+UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5D282B220187F8009EC8ED /* Extension+UIView.swift */; }; + 6A65DA432203D22B005EB2BC /* LoadingView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6A65DA422203D22B005EB2BC /* LoadingView.xib */; }; + 6A65DA452203D242005EB2BC /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A65DA442203D242005EB2BC /* LoadingView.swift */; }; + 6A65DA4A2206190C005EB2BC /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A65DA492206190C005EB2BC /* ImageCache.swift */; }; + 6AE2D7AE21FEF30D00444622 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AE2D7AD21FEF30D00444622 /* Config.swift */; }; + BC19FD13220D94B5006A9C7D /* Enum+TimeIntervalTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC19FD12220D94B5006A9C7D /* Enum+TimeIntervalTypes.swift */; }; + BC19FD15220D965E006A9C7D /* Enum+TimeUnitTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC19FD14220D965E006A9C7D /* Enum+TimeUnitTypes.swift */; }; + BC19FD17220D9D20006A9C7D /* Enum+LocalizedLanguages.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC19FD16220D9D20006A9C7D /* Enum+LocalizedLanguages.swift */; }; BC2656B021FD94E000003413 /* Extension+URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2656AF21FD94E000003413 /* Extension+URL.swift */; }; BC2656BD21FD94EE00003413 /* EventRegistryAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2656B321FD94EE00003413 /* EventRegistryAPI.swift */; }; BC2656BE21FD94EE00003413 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2656B521FD94EE00003413 /* APIService.swift */; }; @@ -28,12 +44,52 @@ BC2656C321FD94EE00003413 /* NetworkResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2656BB21FD94EE00003413 /* NetworkResult.swift */; }; BC2656C421FD94EE00003413 /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2656BC21FD94EE00003413 /* NetworkError.swift */; }; BC2656C721FD950900003413 /* Articles.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2656C621FD950900003413 /* Articles.swift */; }; - BC2656CB21FE97E700003413 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2656CA21FE97E700003413 /* Config.swift */; }; + BC42C038221138C100E0833B /* Enum+HeaderTitles.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC42C037221138C100E0833B /* Enum+HeaderTitles.swift */; }; + BC5D4E41220B1D17000465B0 /* KeywordDetailGraphCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC5D4E3F220B1D17000465B0 /* KeywordDetailGraphCell.swift */; }; + BC5D4E42220B1D17000465B0 /* KeywordDetailGraphCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC5D4E40220B1D17000465B0 /* KeywordDetailGraphCell.xib */; }; + BC5D4E4A220B23C8000465B0 /* Graph.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC5D4E49220B23C8000465B0 /* Graph.swift */; }; + BC5D4E4C220B531D000465B0 /* KeywordDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC5D4E4B220B531D000465B0 /* KeywordDetailViewController.swift */; }; + BC5D4E4E220B730F000465B0 /* GraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC5D4E4D220B730F000465B0 /* GraphView.swift */; }; + BC5D4E50220C247F000465B0 /* NaverAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC5D4E4F220C247E000465B0 /* NaverAPI.swift */; }; + BC63852C2206CBF200074CF3 /* Enum+Countries.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC63852B2206CBF200074CF3 /* Enum+Countries.swift */; }; + BC63852E2207442B00074CF3 /* Extension+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC63852D2207442B00074CF3 /* Extension+Notification.swift */; }; + BC649BA2220F5EFA0032ACF2 /* KeywordDetailArticleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC649BA0220F5EFA0032ACF2 /* KeywordDetailArticleCell.swift */; }; + BC649BA3220F5EFA0032ACF2 /* KeywordDetailArticleCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC649BA1220F5EFA0032ACF2 /* KeywordDetailArticleCell.xib */; }; + BC649BA8220F68230032ACF2 /* HTMLDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC649BA7220F68230032ACF2 /* HTMLDecodable.swift */; }; + BC7969B922002E620003F520 /* TrendDays.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC7969B822002E620003F520 /* TrendDays.swift */; }; + BC7969BC22002EB20003F520 /* Live.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BC7969BB22002EB20003F520 /* Live.storyboard */; }; + BC7969BF22002EC30003F520 /* LiveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC7969BE22002EC30003F520 /* LiveViewController.swift */; }; + BC7969C122002F440003F520 /* GoogleTrendAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC7969C022002F440003F520 /* GoogleTrendAPI.swift */; }; + BC7969C52200AD0E0003F520 /* TrendPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC7969C42200AD0E0003F520 /* TrendPageView.swift */; }; + BC7969C72200AD190003F520 /* TrendPageView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC7969C62200AD190003F520 /* TrendPageView.xib */; }; + BC7969D3220174B40003F520 /* TrendListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC7969D1220174B40003F520 /* TrendListCell.swift */; }; + BC7969D4220174B40003F520 /* TrendListCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC7969D2220174B40003F520 /* TrendListCell.xib */; }; + BC9E02AA2202EBFD00ADD1D5 /* TrendListHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC9E02A82202EBFD00ADD1D5 /* TrendListHeaderCell.swift */; }; + BC9E02AB2202EBFD00ADD1D5 /* TrendListHeaderCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC9E02A92202EBFD00ADD1D5 /* TrendListHeaderCell.xib */; }; + BCD1BB2622057CF800F9C8A8 /* TableViewCell+Animator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD1BB2522057CF800F9C8A8 /* TableViewCell+Animator.swift */; }; + BCDB9E272205A1040095E4E4 /* TrendHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCDB9E252205A1040095E4E4 /* TrendHeaderCell.swift */; }; + BCDB9E282205A1040095E4E4 /* TrendHeaderCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BCDB9E262205A1040095E4E4 /* TrendHeaderCell.xib */; }; D9C6AB3D21FA9B3E001C0AB8 /* Extension+UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C6AB3721FA9B3E001C0AB8 /* Extension+UIColor.swift */; }; D9C6AB4421FA9CF8001C0AB8 /* Search.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D9C6AB4321FA9CF8001C0AB8 /* Search.storyboard */; }; D9C6AB4D21FAAD0A001C0AB8 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C6AB4A21FAAD09001C0AB8 /* SearchViewController.swift */; }; D9C6AB4E21FAAD0A001C0AB8 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C6AB4C21FAAD09001C0AB8 /* MainViewController.swift */; }; D9C6AB5D21FC152D001C0AB8 /* SearchScrollEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C6AB5C21FC152D001C0AB8 /* SearchScrollEnum.swift */; }; + D9C6AB8021FED9F7001C0AB8 /* PresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C6AB7F21FED9F7001C0AB8 /* PresentationController.swift */; }; + D9C6AB8221FEDB08001C0AB8 /* PresentationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C6AB8121FEDB08001C0AB8 /* PresentationManager.swift */; }; + D9C6AB8421FEE6B4001C0AB8 /* SearchFilterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C6AB8321FEE6B4001C0AB8 /* SearchFilterViewController.swift */; }; + D9D145BF220B0B6A00CC58DA /* ScrapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9D145BD220B0B6800CC58DA /* ScrapViewController.swift */; }; + D9D145C0220B0B6A00CC58DA /* ScrapTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9D145BE220B0B6800CC58DA /* ScrapTabBarController.swift */; }; + D9D145C3220B0B7800CC58DA /* TreeData.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D9D145C1220B0B7800CC58DA /* TreeData.xcdatamodeld */; }; + D9D145CA220B0B8400CC58DA /* ScrapService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9D145C5220B0B8400CC58DA /* ScrapService.swift */; }; + D9D145CB220B0B8400CC58DA /* ScrapManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9D145C6220B0B8400CC58DA /* ScrapManager.swift */; }; + D9D145CC220B0B8400CC58DA /* ScrappedArticle+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9D145C8220B0B8400CC58DA /* ScrappedArticle+CoreDataClass.swift */; }; + D9D145CD220B0B8400CC58DA /* ScrappedArticle+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9D145C9220B0B8400CC58DA /* ScrappedArticle+CoreDataProperties.swift */; }; + D9DFFB41220EEA3E0047E326 /* ScrapFilterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9DFFB40220EEA3E0047E326 /* ScrapFilterViewController.swift */; }; + D9DFFB45220EEA7F0047E326 /* ScrapFilterTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9DFFB42220EEA7E0047E326 /* ScrapFilterTableViewCell.swift */; }; + D9DFFB46220EEA7F0047E326 /* ScrapFilterTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D9DFFB43220EEA7E0047E326 /* ScrapFilterTableViewCell.xib */; }; + D9DFFB47220EEA7F0047E326 /* ScrapFilter.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D9DFFB44220EEA7F0047E326 /* ScrapFilter.storyboard */; }; + D9DFFB49220EEA990047E326 /* Scrap.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D9DFFB48220EEA990047E326 /* Scrap.storyboard */; }; + D9DFFB4B220EEADC0047E326 /* ScrapFilterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9DFFB4A220EEADC0047E326 /* ScrapFilterDelegate.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -49,8 +105,24 @@ 35DC0DFE21F6FB3200F30416 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .swiftlint.yml; sourceTree = ""; }; 35DC0E0221F70AB700F30416 /* ArticleDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleDetailViewController.swift; sourceTree = ""; }; 35DC0E0721F70BBC00F30416 /* ArticleDetail.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = ArticleDetail.storyboard; sourceTree = ""; }; + 6A0957AB2208490E00D46741 /* Extension+PickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Extension+PickerView.swift"; sourceTree = ""; }; + 6A0957AD220963B300D46741 /* SearchFilterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFilterProtocol.swift; sourceTree = ""; }; + 6A198CC7220C9F51002421AC /* ArticleTypeEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleTypeEnum.swift; sourceTree = ""; }; + 6A198CC9220CAA9A002421AC /* DefaultLabelView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DefaultLabelView.xib; sourceTree = ""; }; + 6A198CCB220CAAB5002421AC /* DefautLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefautLabelView.swift; sourceTree = ""; }; + 6A47BF41220E0638008BEA7A /* LiveFeedTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveFeedTableViewCell.swift; sourceTree = ""; }; + 6A47BF42220E0638008BEA7A /* LiveFeedTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LiveFeedTableViewCell.xib; sourceTree = ""; }; + 6A5D282922013FA5009EC8ED /* Extension+ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Extension+ImageView.swift"; sourceTree = ""; }; + 6A5D282B220187F8009EC8ED /* Extension+UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Extension+UIView.swift"; sourceTree = ""; }; + 6A65DA422203D22B005EB2BC /* LoadingView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LoadingView.xib; sourceTree = ""; }; + 6A65DA442203D242005EB2BC /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; + 6A65DA492206190C005EB2BC /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; + 6AE2D7AD21FEF30D00444622 /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; 8965743E41477F79773392B1 /* Pods-tree.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-tree.debug.xcconfig"; path = "Pods/Target Support Files/Pods-tree/Pods-tree.debug.xcconfig"; sourceTree = ""; }; 9833F533FEAC2BBB2BE859BF /* Pods-tree.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-tree.release.xcconfig"; path = "Pods/Target Support Files/Pods-tree/Pods-tree.release.xcconfig"; sourceTree = ""; }; + BC19FD12220D94B5006A9C7D /* Enum+TimeIntervalTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Enum+TimeIntervalTypes.swift"; sourceTree = ""; }; + BC19FD14220D965E006A9C7D /* Enum+TimeUnitTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Enum+TimeUnitTypes.swift"; sourceTree = ""; }; + BC19FD16220D9D20006A9C7D /* Enum+LocalizedLanguages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Enum+LocalizedLanguages.swift"; sourceTree = ""; }; BC2656AF21FD94E000003413 /* Extension+URL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Extension+URL.swift"; sourceTree = ""; }; BC2656B321FD94EE00003413 /* EventRegistryAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventRegistryAPI.swift; sourceTree = ""; }; BC2656B521FD94EE00003413 /* APIService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; @@ -61,13 +133,53 @@ BC2656BB21FD94EE00003413 /* NetworkResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkResult.swift; sourceTree = ""; }; BC2656BC21FD94EE00003413 /* NetworkError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; BC2656C621FD950900003413 /* Articles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Articles.swift; sourceTree = ""; }; - BC2656CA21FE97E700003413 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Config.swift; path = ../../../../Documents/Config.swift; sourceTree = ""; }; + BC42C037221138C100E0833B /* Enum+HeaderTitles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Enum+HeaderTitles.swift"; sourceTree = ""; }; + BC5D4E3F220B1D17000465B0 /* KeywordDetailGraphCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordDetailGraphCell.swift; sourceTree = ""; }; + BC5D4E40220B1D17000465B0 /* KeywordDetailGraphCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = KeywordDetailGraphCell.xib; sourceTree = ""; }; + BC5D4E49220B23C8000465B0 /* Graph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Graph.swift; sourceTree = ""; }; + BC5D4E4B220B531D000465B0 /* KeywordDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordDetailViewController.swift; sourceTree = ""; }; + BC5D4E4D220B730F000465B0 /* GraphView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphView.swift; sourceTree = ""; }; + BC5D4E4F220C247E000465B0 /* NaverAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NaverAPI.swift; sourceTree = ""; }; + BC63852B2206CBF200074CF3 /* Enum+Countries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Enum+Countries.swift"; sourceTree = ""; }; + BC63852D2207442B00074CF3 /* Extension+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Extension+Notification.swift"; sourceTree = ""; }; + BC649BA0220F5EFA0032ACF2 /* KeywordDetailArticleCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordDetailArticleCell.swift; sourceTree = ""; }; + BC649BA1220F5EFA0032ACF2 /* KeywordDetailArticleCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = KeywordDetailArticleCell.xib; sourceTree = ""; }; + BC649BA7220F68230032ACF2 /* HTMLDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLDecodable.swift; sourceTree = ""; }; + BC7969B822002E620003F520 /* TrendDays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendDays.swift; sourceTree = ""; }; + BC7969BB22002EB20003F520 /* Live.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Live.storyboard; sourceTree = ""; }; + BC7969BE22002EC30003F520 /* LiveViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveViewController.swift; sourceTree = ""; }; + BC7969C022002F440003F520 /* GoogleTrendAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleTrendAPI.swift; sourceTree = ""; }; + BC7969C42200AD0E0003F520 /* TrendPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendPageView.swift; sourceTree = ""; }; + BC7969C62200AD190003F520 /* TrendPageView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TrendPageView.xib; sourceTree = ""; }; + BC7969D1220174B40003F520 /* TrendListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendListCell.swift; sourceTree = ""; }; + BC7969D2220174B40003F520 /* TrendListCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TrendListCell.xib; sourceTree = ""; }; + BC9E02A82202EBFD00ADD1D5 /* TrendListHeaderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendListHeaderCell.swift; sourceTree = ""; }; + BC9E02A92202EBFD00ADD1D5 /* TrendListHeaderCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TrendListHeaderCell.xib; sourceTree = ""; }; + BCD1BB2522057CF800F9C8A8 /* TableViewCell+Animator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TableViewCell+Animator.swift"; sourceTree = ""; }; + BCDB9E252205A1040095E4E4 /* TrendHeaderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendHeaderCell.swift; sourceTree = ""; }; + BCDB9E262205A1040095E4E4 /* TrendHeaderCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TrendHeaderCell.xib; sourceTree = ""; }; BF0163F8362378789ACC30D4 /* Pods_tree.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_tree.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D9C6AB3721FA9B3E001C0AB8 /* Extension+UIColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Extension+UIColor.swift"; sourceTree = ""; }; D9C6AB4321FA9CF8001C0AB8 /* Search.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Search.storyboard; sourceTree = ""; }; D9C6AB4A21FAAD09001C0AB8 /* SearchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; D9C6AB4C21FAAD09001C0AB8 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; D9C6AB5C21FC152D001C0AB8 /* SearchScrollEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchScrollEnum.swift; sourceTree = ""; }; + D9C6AB7F21FED9F7001C0AB8 /* PresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentationController.swift; sourceTree = ""; }; + D9C6AB8121FEDB08001C0AB8 /* PresentationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentationManager.swift; sourceTree = ""; }; + D9C6AB8321FEE6B4001C0AB8 /* SearchFilterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFilterViewController.swift; sourceTree = ""; }; + D9D145BD220B0B6800CC58DA /* ScrapViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrapViewController.swift; sourceTree = ""; }; + D9D145BE220B0B6800CC58DA /* ScrapTabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrapTabBarController.swift; sourceTree = ""; }; + D9D145C2220B0B7800CC58DA /* TreeData.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TreeData.xcdatamodel; sourceTree = ""; }; + D9D145C5220B0B8400CC58DA /* ScrapService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrapService.swift; sourceTree = ""; }; + D9D145C6220B0B8400CC58DA /* ScrapManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrapManager.swift; sourceTree = ""; }; + D9D145C8220B0B8400CC58DA /* ScrappedArticle+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ScrappedArticle+CoreDataClass.swift"; sourceTree = ""; }; + D9D145C9220B0B8400CC58DA /* ScrappedArticle+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ScrappedArticle+CoreDataProperties.swift"; sourceTree = ""; }; + D9DFFB40220EEA3E0047E326 /* ScrapFilterViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrapFilterViewController.swift; sourceTree = ""; }; + D9DFFB42220EEA7E0047E326 /* ScrapFilterTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrapFilterTableViewCell.swift; sourceTree = ""; }; + D9DFFB43220EEA7E0047E326 /* ScrapFilterTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ScrapFilterTableViewCell.xib; sourceTree = ""; }; + D9DFFB44220EEA7F0047E326 /* ScrapFilter.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ScrapFilter.storyboard; sourceTree = ""; }; + D9DFFB48220EEA990047E326 /* Scrap.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Scrap.storyboard; sourceTree = ""; }; + D9DFFB4A220EEADC0047E326 /* ScrapFilterDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ScrapFilterDelegate.swift; path = tree/Helper/ScrapFilterDelegate.swift; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -112,23 +224,50 @@ 35DC0DCD21F6F3C700F30416 /* tree */ = { isa = PBXGroup; children = ( - BC2656C921FE97D200003413 /* Config */, + 6AE2D7AC21FEF30D00444622 /* Config */, BC2656C521FD950900003413 /* Model */, + BC2656B121FD94EE00003413 /* Network Layer */, D9C6AB3F21FA9CB4001C0AB8 /* View */, D9C6AB4821FAAD09001C0AB8 /* Controller */, D9C6AB3621FA9B3E001C0AB8 /* Extension */, - BC2656B121FD94EE00003413 /* Network Layer */, D9C6AB5B21FC151C001C0AB8 /* Helper */, + BC649BA6220F68110032ACF2 /* Protocol */, 35DC0DCE21F6F3C700F30416 /* AppDelegate.swift */, 35DC0DD221F6F3C700F30416 /* Main.storyboard */, 35DC0DD521F6F3CB00F30416 /* Assets.xcassets */, 35DC0DD721F6F3CB00F30416 /* LaunchScreen.storyboard */, 35DC0DDA21F6F3CB00F30416 /* Info.plist */, + D9D145C1220B0B7800CC58DA /* TreeData.xcdatamodeld */, 35DC0DFE21F6FB3200F30416 /* .swiftlint.yml */, ); path = tree; sourceTree = ""; }; + 6A65DA412203D202005EB2BC /* Helper */ = { + isa = PBXGroup; + children = ( + 6A65DA422203D22B005EB2BC /* LoadingView.xib */, + 6A65DA442203D242005EB2BC /* LoadingView.swift */, + ); + path = Helper; + sourceTree = ""; + }; + 6A65DA4B2206D533005EB2BC /* ImageHelper */ = { + isa = PBXGroup; + children = ( + 6A65DA492206190C005EB2BC /* ImageCache.swift */, + ); + path = ImageHelper; + sourceTree = ""; + }; + 6AE2D7AC21FEF30D00444622 /* Config */ = { + isa = PBXGroup; + children = ( + 6AE2D7AD21FEF30D00444622 /* Config.swift */, + ); + path = Config; + sourceTree = ""; + }; 968D39AD37EEED1A9F7CF6F0 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -151,6 +290,8 @@ isa = PBXGroup; children = ( BC2656B321FD94EE00003413 /* EventRegistryAPI.swift */, + BC7969C022002F440003F520 /* GoogleTrendAPI.swift */, + BC5D4E4F220C247E000465B0 /* NaverAPI.swift */, ); path = API; sourceTree = ""; @@ -159,8 +300,8 @@ isa = PBXGroup; children = ( BC2656B521FD94EE00003413 /* APIService.swift */, - BC2656B621FD94EE00003413 /* APIManager.swift */, BC2656B721FD94EE00003413 /* APICenter.swift */, + BC2656B621FD94EE00003413 /* APIManager.swift */, ); path = Service; sourceTree = ""; @@ -179,17 +320,78 @@ BC2656C521FD950900003413 /* Model */ = { isa = PBXGroup; children = ( + D9D145C7220B0B8400CC58DA /* CoreDataModel */, + D9D145C4220B0B8400CC58DA /* DataManager */, BC2656C621FD950900003413 /* Articles.swift */, + BC7969B822002E620003F520 /* TrendDays.swift */, + BC5D4E49220B23C8000465B0 /* Graph.swift */, ); path = Model; sourceTree = ""; }; - BC2656C921FE97D200003413 /* Config */ = { + BC5D4E44220B1D60000465B0 /* Keyword */ = { + isa = PBXGroup; + children = ( + BCDB9E252205A1040095E4E4 /* TrendHeaderCell.swift */, + BCDB9E262205A1040095E4E4 /* TrendHeaderCell.xib */, + BC7969D1220174B40003F520 /* TrendListCell.swift */, + BC7969D2220174B40003F520 /* TrendListCell.xib */, + BC9E02A82202EBFD00ADD1D5 /* TrendListHeaderCell.swift */, + BC9E02A92202EBFD00ADD1D5 /* TrendListHeaderCell.xib */, + BC5D4E46220B1D6C000465B0 /* Detail */, + ); + path = Keyword; + sourceTree = ""; + }; + BC5D4E45220B1D66000465B0 /* Events */ = { isa = PBXGroup; children = ( - BC2656CA21FE97E700003413 /* Config.swift */, ); - path = Config; + path = Events; + sourceTree = ""; + }; + BC5D4E46220B1D6C000465B0 /* Detail */ = { + isa = PBXGroup; + children = ( + BC5D4E3F220B1D17000465B0 /* KeywordDetailGraphCell.swift */, + BC5D4E40220B1D17000465B0 /* KeywordDetailGraphCell.xib */, + BC649BA0220F5EFA0032ACF2 /* KeywordDetailArticleCell.swift */, + BC649BA1220F5EFA0032ACF2 /* KeywordDetailArticleCell.xib */, + BC5D4E4D220B730F000465B0 /* GraphView.swift */, + ); + path = Detail; + sourceTree = ""; + }; + BC649BA6220F68110032ACF2 /* Protocol */ = { + isa = PBXGroup; + children = ( + BC649BA7220F68230032ACF2 /* HTMLDecodable.swift */, + ); + path = Protocol; + sourceTree = ""; + }; + BC7969BA22002EA60003F520 /* Live */ = { + isa = PBXGroup; + children = ( + BC7969BB22002EB20003F520 /* Live.storyboard */, + BC7969C42200AD0E0003F520 /* TrendPageView.swift */, + BC7969C62200AD190003F520 /* TrendPageView.xib */, + BC5D4E45220B1D66000465B0 /* Events */, + BC5D4E44220B1D60000465B0 /* Keyword */, + BCD1BB2522057CF800F9C8A8 /* TableViewCell+Animator.swift */, + 6A47BF41220E0638008BEA7A /* LiveFeedTableViewCell.swift */, + 6A47BF42220E0638008BEA7A /* LiveFeedTableViewCell.xib */, + ); + path = Live; + sourceTree = ""; + }; + BC7969BD22002EB70003F520 /* Live */ = { + isa = PBXGroup; + children = ( + BC7969BE22002EC30003F520 /* LiveViewController.swift */, + BC5D4E4B220B531D000465B0 /* KeywordDetailViewController.swift */, + ); + path = Live; sourceTree = ""; }; D9C6AB3621FA9B3E001C0AB8 /* Extension */ = { @@ -197,6 +399,10 @@ children = ( BC2656AF21FD94E000003413 /* Extension+URL.swift */, D9C6AB3721FA9B3E001C0AB8 /* Extension+UIColor.swift */, + 6A5D282922013FA5009EC8ED /* Extension+ImageView.swift */, + 6A5D282B220187F8009EC8ED /* Extension+UIView.swift */, + BC63852D2207442B00074CF3 /* Extension+Notification.swift */, + 6A0957AB2208490E00D46741 /* Extension+PickerView.swift */, ); path = Extension; sourceTree = ""; @@ -204,6 +410,9 @@ D9C6AB3F21FA9CB4001C0AB8 /* View */ = { isa = PBXGroup; children = ( + D9D145CE220B0B9300CC58DA /* Scrap */, + 6A65DA412203D202005EB2BC /* Helper */, + BC7969BA22002EA60003F520 /* Live */, D9C6AB4021FA9CBB001C0AB8 /* Search */, ); path = View; @@ -216,6 +425,8 @@ D9C6AB4321FA9CF8001C0AB8 /* Search.storyboard */, 35CB481E21FB6306000D96CF /* ArticleFeedTableViewCell.swift */, 35CB481F21FB6306000D96CF /* ArticleFeedTableViewCell.xib */, + 6A198CC9220CAA9A002421AC /* DefaultLabelView.xib */, + 6A198CCB220CAAB5002421AC /* DefautLabelView.swift */, ); path = Search; sourceTree = ""; @@ -223,7 +434,9 @@ D9C6AB4821FAAD09001C0AB8 /* Controller */ = { isa = PBXGroup; children = ( + BC7969BD22002EB70003F520 /* Live */, D9C6AB4921FAAD09001C0AB8 /* Search */, + D9D145BC220B0B6800CC58DA /* Scrap */, D9C6AB4B21FAAD09001C0AB8 /* Main */, ); path = Controller; @@ -234,6 +447,7 @@ children = ( 35DC0E0221F70AB700F30416 /* ArticleDetailViewController.swift */, D9C6AB4A21FAAD09001C0AB8 /* SearchViewController.swift */, + D9C6AB8321FEE6B4001C0AB8 /* SearchFilterViewController.swift */, 35CB481C21FAE7C8000D96CF /* ArticleImageViewController.swift */, ); path = Search; @@ -250,11 +464,69 @@ D9C6AB5B21FC151C001C0AB8 /* Helper */ = { isa = PBXGroup; children = ( + 6A65DA4B2206D533005EB2BC /* ImageHelper */, + D9C6AB7E21FED67A001C0AB8 /* PresentationManager */, D9C6AB5C21FC152D001C0AB8 /* SearchScrollEnum.swift */, + BC63852B2206CBF200074CF3 /* Enum+Countries.swift */, + 6A0957AD220963B300D46741 /* SearchFilterProtocol.swift */, + BC19FD12220D94B5006A9C7D /* Enum+TimeIntervalTypes.swift */, + BC19FD14220D965E006A9C7D /* Enum+TimeUnitTypes.swift */, + BC19FD16220D9D20006A9C7D /* Enum+LocalizedLanguages.swift */, + 6A198CC7220C9F51002421AC /* ArticleTypeEnum.swift */, + BC42C037221138C100E0833B /* Enum+HeaderTitles.swift */, ); path = Helper; sourceTree = ""; }; + D9C6AB7E21FED67A001C0AB8 /* PresentationManager */ = { + isa = PBXGroup; + children = ( + D9DFFB4A220EEADC0047E326 /* ScrapFilterDelegate.swift */, + D9C6AB7F21FED9F7001C0AB8 /* PresentationController.swift */, + D9C6AB8121FEDB08001C0AB8 /* PresentationManager.swift */, + ); + path = PresentationManager; + sourceTree = ""; + }; + D9D145BC220B0B6800CC58DA /* Scrap */ = { + isa = PBXGroup; + children = ( + D9DFFB40220EEA3E0047E326 /* ScrapFilterViewController.swift */, + D9D145BD220B0B6800CC58DA /* ScrapViewController.swift */, + D9D145BE220B0B6800CC58DA /* ScrapTabBarController.swift */, + ); + path = Scrap; + sourceTree = ""; + }; + D9D145C4220B0B8400CC58DA /* DataManager */ = { + isa = PBXGroup; + children = ( + D9D145C5220B0B8400CC58DA /* ScrapService.swift */, + D9D145C6220B0B8400CC58DA /* ScrapManager.swift */, + ); + path = DataManager; + sourceTree = ""; + }; + D9D145C7220B0B8400CC58DA /* CoreDataModel */ = { + isa = PBXGroup; + children = ( + D9D145C8220B0B8400CC58DA /* ScrappedArticle+CoreDataClass.swift */, + D9D145C9220B0B8400CC58DA /* ScrappedArticle+CoreDataProperties.swift */, + ); + path = CoreDataModel; + sourceTree = ""; + }; + D9D145CE220B0B9300CC58DA /* Scrap */ = { + isa = PBXGroup; + children = ( + D9DFFB44220EEA7F0047E326 /* ScrapFilter.storyboard */, + D9DFFB42220EEA7E0047E326 /* ScrapFilterTableViewCell.swift */, + D9DFFB43220EEA7E0047E326 /* ScrapFilterTableViewCell.xib */, + D9DFFB48220EEA990047E326 /* Scrap.storyboard */, + ); + path = Scrap; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -315,13 +587,26 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + D9DFFB47220EEA7F0047E326 /* ScrapFilter.storyboard in Resources */, + BC5D4E42220B1D17000465B0 /* KeywordDetailGraphCell.xib in Resources */, + 6A47BF44220E0638008BEA7A /* LiveFeedTableViewCell.xib in Resources */, 35DC0DFF21F6FB3200F30416 /* .swiftlint.yml in Resources */, + 6A65DA432203D22B005EB2BC /* LoadingView.xib in Resources */, + BCDB9E282205A1040095E4E4 /* TrendHeaderCell.xib in Resources */, 35DC0DD921F6F3CB00F30416 /* LaunchScreen.storyboard in Resources */, + BC9E02AB2202EBFD00ADD1D5 /* TrendListHeaderCell.xib in Resources */, 35CB482121FB6306000D96CF /* ArticleFeedTableViewCell.xib in Resources */, 35DC0E0821F70BBC00F30416 /* ArticleDetail.storyboard in Resources */, + 6A198CCA220CAA9A002421AC /* DefaultLabelView.xib in Resources */, + BC7969C72200AD190003F520 /* TrendPageView.xib in Resources */, 35DC0DD621F6F3CB00F30416 /* Assets.xcassets in Resources */, + D9DFFB46220EEA7F0047E326 /* ScrapFilterTableViewCell.xib in Resources */, 35DC0DD421F6F3C700F30416 /* Main.storyboard in Resources */, + BC7969BC22002EB20003F520 /* Live.storyboard in Resources */, + BC649BA3220F5EFA0032ACF2 /* KeywordDetailArticleCell.xib in Resources */, D9C6AB4421FA9CF8001C0AB8 /* Search.storyboard in Resources */, + D9DFFB49220EEA990047E326 /* Scrap.storyboard in Resources */, + BC7969D4220174B40003F520 /* TrendListCell.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -374,25 +659,68 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6A198CCC220CAAB5002421AC /* DefautLabelView.swift in Sources */, + D9C6AB8421FEE6B4001C0AB8 /* SearchFilterViewController.swift in Sources */, + D9D145CD220B0B8400CC58DA /* ScrappedArticle+CoreDataProperties.swift in Sources */, BC2656C421FD94EE00003413 /* NetworkError.swift in Sources */, + BC19FD15220D965E006A9C7D /* Enum+TimeUnitTypes.swift in Sources */, + 6A5D282C220187F8009EC8ED /* Extension+UIView.swift in Sources */, BC2656BF21FD94EE00003413 /* APIManager.swift in Sources */, 35CB482021FB6306000D96CF /* ArticleFeedTableViewCell.swift in Sources */, + BC649BA8220F68230032ACF2 /* HTMLDecodable.swift in Sources */, + 6A65DA4A2206190C005EB2BC /* ImageCache.swift in Sources */, + BC5D4E4A220B23C8000465B0 /* Graph.swift in Sources */, + BC19FD17220D9D20006A9C7D /* Enum+LocalizedLanguages.swift in Sources */, D9C6AB4D21FAAD0A001C0AB8 /* SearchViewController.swift in Sources */, - BC2656CB21FE97E700003413 /* Config.swift in Sources */, + D9D145C0220B0B6A00CC58DA /* ScrapTabBarController.swift in Sources */, + BC7969B922002E620003F520 /* TrendDays.swift in Sources */, BC2656C721FD950900003413 /* Articles.swift in Sources */, + BC5D4E41220B1D17000465B0 /* KeywordDetailGraphCell.swift in Sources */, D9C6AB5D21FC152D001C0AB8 /* SearchScrollEnum.swift in Sources */, + 6A47BF43220E0638008BEA7A /* LiveFeedTableViewCell.swift in Sources */, + BC5D4E4C220B531D000465B0 /* KeywordDetailViewController.swift in Sources */, D9C6AB3D21FA9B3E001C0AB8 /* Extension+UIColor.swift in Sources */, + 6A0957AE220963B300D46741 /* SearchFilterProtocol.swift in Sources */, + BC7969D3220174B40003F520 /* TrendListCell.swift in Sources */, + BC19FD13220D94B5006A9C7D /* Enum+TimeIntervalTypes.swift in Sources */, BC2656BE21FD94EE00003413 /* APIService.swift in Sources */, + 6A0957AC2208490E00D46741 /* Extension+PickerView.swift in Sources */, + BC9E02AA2202EBFD00ADD1D5 /* TrendListHeaderCell.swift in Sources */, BC2656BD21FD94EE00003413 /* EventRegistryAPI.swift in Sources */, + BC7969C52200AD0E0003F520 /* TrendPageView.swift in Sources */, + D9C6AB8221FEDB08001C0AB8 /* PresentationManager.swift in Sources */, + D9DFFB4B220EEADC0047E326 /* ScrapFilterDelegate.swift in Sources */, BC2656C321FD94EE00003413 /* NetworkResult.swift in Sources */, BC2656C021FD94EE00003413 /* APICenter.swift in Sources */, + BC63852E2207442B00074CF3 /* Extension+Notification.swift in Sources */, + D9DFFB45220EEA7F0047E326 /* ScrapFilterTableViewCell.swift in Sources */, BC2656C221FD94EE00003413 /* ParametersEncoder.swift in Sources */, + D9DFFB41220EEA3E0047E326 /* ScrapFilterViewController.swift in Sources */, BC2656B021FD94E000003413 /* Extension+URL.swift in Sources */, BC2656C121FD94EE00003413 /* NetworkResponse.swift in Sources */, + BC5D4E4E220B730F000465B0 /* GraphView.swift in Sources */, 35DC0DCF21F6F3C700F30416 /* AppDelegate.swift in Sources */, + 6A65DA452203D242005EB2BC /* LoadingView.swift in Sources */, + BC649BA2220F5EFA0032ACF2 /* KeywordDetailArticleCell.swift in Sources */, + BC5D4E50220C247F000465B0 /* NaverAPI.swift in Sources */, + BC7969BF22002EC30003F520 /* LiveViewController.swift in Sources */, + BC63852C2206CBF200074CF3 /* Enum+Countries.swift in Sources */, + D9D145C3220B0B7800CC58DA /* TreeData.xcdatamodeld in Sources */, D9C6AB4E21FAAD0A001C0AB8 /* MainViewController.swift in Sources */, + 6AE2D7AE21FEF30D00444622 /* Config.swift in Sources */, + BC42C038221138C100E0833B /* Enum+HeaderTitles.swift in Sources */, + BCD1BB2622057CF800F9C8A8 /* TableViewCell+Animator.swift in Sources */, + D9D145CB220B0B8400CC58DA /* ScrapManager.swift in Sources */, 35CB481D21FAE7C8000D96CF /* ArticleImageViewController.swift in Sources */, + 6A5D282A22013FA5009EC8ED /* Extension+ImageView.swift in Sources */, + BCDB9E272205A1040095E4E4 /* TrendHeaderCell.swift in Sources */, + 6A198CC8220C9F51002421AC /* ArticleTypeEnum.swift in Sources */, + D9D145CA220B0B8400CC58DA /* ScrapService.swift in Sources */, + D9D145BF220B0B6A00CC58DA /* ScrapViewController.swift in Sources */, + D9D145CC220B0B8400CC58DA /* ScrappedArticle+CoreDataClass.swift in Sources */, 35CB481B21FAC610000D96CF /* ArticleDetailViewController.swift in Sources */, + BC7969C122002F440003F520 /* GoogleTrendAPI.swift in Sources */, + D9C6AB8021FED9F7001C0AB8 /* PresentationController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -542,6 +870,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = tree/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -561,6 +890,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = tree/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -594,6 +924,19 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCVersionGroup section */ + D9D145C1220B0B7800CC58DA /* TreeData.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + D9D145C2220B0B7800CC58DA /* TreeData.xcdatamodel */, + ); + currentVersion = D9D145C2220B0B7800CC58DA /* TreeData.xcdatamodel */; + path = TreeData.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ }; rootObject = 35DC0DC321F6F3C700F30416 /* Project object */; } diff --git a/tree/tree.xcodeproj/xcshareddata/xcschemes/tree.xcscheme b/tree/tree.xcodeproj/xcshareddata/xcschemes/tree.xcscheme new file mode 100644 index 0000000..62c907e --- /dev/null +++ b/tree/tree.xcodeproj/xcshareddata/xcschemes/tree.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tree/tree/.swiftlint.yml b/tree/tree/.swiftlint.yml index c67ea7c..1ee13df 100644 --- a/tree/tree/.swiftlint.yml +++ b/tree/tree/.swiftlint.yml @@ -1,5 +1,6 @@ disabled_rules: - line_length +- trailing_whitespace included: excluded: - Pods diff --git a/tree/tree/AppDelegate.swift b/tree/tree/AppDelegate.swift index e12c192..6a65e1b 100644 --- a/tree/tree/AppDelegate.swift +++ b/tree/tree/AppDelegate.swift @@ -7,6 +7,7 @@ // import UIKit +import CoreData @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @@ -14,7 +15,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. + if UserDefaults.standard.dictionary(forKey: "searchFilter") == nil { + let searchFilter = ["keyword": "Title","sort": "Date","category": "All","language": "eng"] + UserDefaults.standard.set(searchFilter, forKey: "searchFilter") + } return true } @@ -34,6 +38,30 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } func applicationWillTerminate(_ application: UIApplication) { + self.saveContext() + } + + // MARK: - Core Data stack + lazy var persistentContainer: NSPersistentContainer = { + let container = NSPersistentContainer(name: "TreeData") + container.loadPersistentStores(completionHandler: { (_, error) in + if let error = error as NSError? { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + return container + }() + + private func saveContext () { + let context = persistentContainer.viewContext + if context.hasChanges { + do { + try context.save() + } catch { + let nserror = error as NSError + fatalError("Unresolved error \(nserror), \(nserror.userInfo)") + } + } } } diff --git a/tree/tree/Assets.xcassets/2.imageset/2.jpeg b/tree/tree/Assets.xcassets/2.imageset/2.jpeg deleted file mode 100644 index 8568a77..0000000 Binary files a/tree/tree/Assets.xcassets/2.imageset/2.jpeg and /dev/null differ diff --git a/tree/tree/Assets.xcassets/2.imageset/Contents.json b/tree/tree/Assets.xcassets/Live.imageset/Contents.json similarity index 72% rename from tree/tree/Assets.xcassets/2.imageset/Contents.json rename to tree/tree/Assets.xcassets/Live.imageset/Contents.json index 410262e..fd0228a 100644 --- a/tree/tree/Assets.xcassets/2.imageset/Contents.json +++ b/tree/tree/Assets.xcassets/Live.imageset/Contents.json @@ -2,15 +2,17 @@ "images" : [ { "idiom" : "universal", - "filename" : "2.jpeg", + "filename" : "Live@1x.png", "scale" : "1x" }, { "idiom" : "universal", + "filename" : "Live@2x.png", "scale" : "2x" }, { "idiom" : "universal", + "filename" : "Live@3x.png", "scale" : "3x" } ], diff --git a/tree/tree/Assets.xcassets/Live.imageset/Live@1x.png b/tree/tree/Assets.xcassets/Live.imageset/Live@1x.png new file mode 100644 index 0000000..3169966 Binary files /dev/null and b/tree/tree/Assets.xcassets/Live.imageset/Live@1x.png differ diff --git a/tree/tree/Assets.xcassets/Live.imageset/Live@2x.png b/tree/tree/Assets.xcassets/Live.imageset/Live@2x.png new file mode 100644 index 0000000..efa8855 Binary files /dev/null and b/tree/tree/Assets.xcassets/Live.imageset/Live@2x.png differ diff --git a/tree/tree/Assets.xcassets/Live.imageset/Live@3x.png b/tree/tree/Assets.xcassets/Live.imageset/Live@3x.png new file mode 100644 index 0000000..3f4d549 Binary files /dev/null and b/tree/tree/Assets.xcassets/Live.imageset/Live@3x.png differ diff --git a/tree/tree/Assets.xcassets/Scrap.imageset/Contents.json b/tree/tree/Assets.xcassets/Scrap.imageset/Contents.json new file mode 100644 index 0000000..508eda9 --- /dev/null +++ b/tree/tree/Assets.xcassets/Scrap.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Scrap@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Scrap@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Scrap@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/tree/tree/Assets.xcassets/Scrap.imageset/Scrap@1x.png b/tree/tree/Assets.xcassets/Scrap.imageset/Scrap@1x.png new file mode 100644 index 0000000..b575838 Binary files /dev/null and b/tree/tree/Assets.xcassets/Scrap.imageset/Scrap@1x.png differ diff --git a/tree/tree/Assets.xcassets/Scrap.imageset/Scrap@2x.png b/tree/tree/Assets.xcassets/Scrap.imageset/Scrap@2x.png new file mode 100644 index 0000000..a696cfd Binary files /dev/null and b/tree/tree/Assets.xcassets/Scrap.imageset/Scrap@2x.png differ diff --git a/tree/tree/Assets.xcassets/Scrap.imageset/Scrap@3x.png b/tree/tree/Assets.xcassets/Scrap.imageset/Scrap@3x.png new file mode 100644 index 0000000..8ddc0bf Binary files /dev/null and b/tree/tree/Assets.xcassets/Scrap.imageset/Scrap@3x.png differ diff --git a/tree/tree/Assets.xcassets/Search.imageset/Contents.json b/tree/tree/Assets.xcassets/Search.imageset/Contents.json new file mode 100644 index 0000000..49aedd3 --- /dev/null +++ b/tree/tree/Assets.xcassets/Search.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Search@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Search@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Search@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/tree/tree/Assets.xcassets/Search.imageset/Search@1x.png b/tree/tree/Assets.xcassets/Search.imageset/Search@1x.png new file mode 100644 index 0000000..87e74ff Binary files /dev/null and b/tree/tree/Assets.xcassets/Search.imageset/Search@1x.png differ diff --git a/tree/tree/Assets.xcassets/Search.imageset/Search@2x.png b/tree/tree/Assets.xcassets/Search.imageset/Search@2x.png new file mode 100644 index 0000000..090acd4 Binary files /dev/null and b/tree/tree/Assets.xcassets/Search.imageset/Search@2x.png differ diff --git a/tree/tree/Assets.xcassets/Search.imageset/Search@3x.png b/tree/tree/Assets.xcassets/Search.imageset/Search@3x.png new file mode 100644 index 0000000..96f68e2 Binary files /dev/null and b/tree/tree/Assets.xcassets/Search.imageset/Search@3x.png differ diff --git a/tree/tree/Assets.xcassets/back.imageset/Contents.json b/tree/tree/Assets.xcassets/back.imageset/Contents.json new file mode 100644 index 0000000..c3c8ac6 --- /dev/null +++ b/tree/tree/Assets.xcassets/back.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "left-arrow.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "left-arrow@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "left-arrow@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/tree/tree/Assets.xcassets/back.imageset/left-arrow.png b/tree/tree/Assets.xcassets/back.imageset/left-arrow.png new file mode 100644 index 0000000..37083e7 Binary files /dev/null and b/tree/tree/Assets.xcassets/back.imageset/left-arrow.png differ diff --git a/tree/tree/Assets.xcassets/back.imageset/left-arrow@2x.png b/tree/tree/Assets.xcassets/back.imageset/left-arrow@2x.png new file mode 100644 index 0000000..e0155a3 Binary files /dev/null and b/tree/tree/Assets.xcassets/back.imageset/left-arrow@2x.png differ diff --git a/tree/tree/Assets.xcassets/back.imageset/left-arrow@3x.png b/tree/tree/Assets.xcassets/back.imageset/left-arrow@3x.png new file mode 100644 index 0000000..c288e20 Binary files /dev/null and b/tree/tree/Assets.xcassets/back.imageset/left-arrow@3x.png differ diff --git a/tree/tree/Assets.xcassets/down.imageset/Contents.json b/tree/tree/Assets.xcassets/down.imageset/Contents.json new file mode 100644 index 0000000..42746d7 --- /dev/null +++ b/tree/tree/Assets.xcassets/down.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "down@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "down@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "down@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/tree/tree/Assets.xcassets/down.imageset/down@1x.png b/tree/tree/Assets.xcassets/down.imageset/down@1x.png new file mode 100644 index 0000000..2a6f560 Binary files /dev/null and b/tree/tree/Assets.xcassets/down.imageset/down@1x.png differ diff --git a/tree/tree/Assets.xcassets/down.imageset/down@2x.png b/tree/tree/Assets.xcassets/down.imageset/down@2x.png new file mode 100644 index 0000000..f984862 Binary files /dev/null and b/tree/tree/Assets.xcassets/down.imageset/down@2x.png differ diff --git a/tree/tree/Assets.xcassets/down.imageset/down@3x.png b/tree/tree/Assets.xcassets/down.imageset/down@3x.png new file mode 100644 index 0000000..2b86cf6 Binary files /dev/null and b/tree/tree/Assets.xcassets/down.imageset/down@3x.png differ diff --git a/tree/tree/Base.lproj/Main.storyboard b/tree/tree/Base.lproj/Main.storyboard index f1bcf38..44cceb1 100644 --- a/tree/tree/Base.lproj/Main.storyboard +++ b/tree/tree/Base.lproj/Main.storyboard @@ -1,24 +1,67 @@ - + + + + - - + - - + + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tree/tree/Controller/Live/KeywordDetailViewController.swift b/tree/tree/Controller/Live/KeywordDetailViewController.swift new file mode 100644 index 0000000..6180ccc --- /dev/null +++ b/tree/tree/Controller/Live/KeywordDetailViewController.swift @@ -0,0 +1,182 @@ +// +// KeywordDetailViewController.swift +// tree +// +// Created by ParkSungJoon on 07/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +class KeywordDetailViewController: UIViewController { + + @IBOutlet var tableView: UITableView! + @IBOutlet weak var navigationBar: UINavigationBar! + + var keywordData: TrendingSearch? { + didSet { + guard let keyword = keywordData?.title.query else { return } + loadGraphData(to: keyword, startDate, endDate) + articleData = keywordData?.articles + } + } + var graphData: Graph? { + didSet { + DispatchQueue.main.async { + self.tableView.reloadData() + } + } + } + private var startDate: String { + let oneMonth = TimeIntervalTypes.oneMonth.value + let date = Date(timeInterval: -oneMonth, since: Date()) + return dateFormatter.string(from: date) + } + private var endDate: String { + let oneDay = TimeIntervalTypes.oneDay.value + let date = Date(timeInterval: -oneDay, since: Date()) + return dateFormatter.string(from: date) + } + private var articleData: [KeywordArticles]? { + didSet { + DispatchQueue.main.async { + self.tableView.reloadSections(IndexSet(arrayLiteral: 1), with: .automatic) + } + } + } + private let timeUnit = TimeUnitTypes.week.value + private let graphCellIdentifier = "KeywordDetailGraphCell" + private let headerCellIdentifier = "TrendListHeaderCell" + private let articleCellIdentifier = "KeywordDetailArticleCell" + private let appleGothicNeoBold = "AppleSDGothicNeo-Bold" + private let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + return formatter + }() + + override func viewDidLoad() { + super.viewDidLoad() + registerXIB() + tableViewSetup() + navigationBar.topItem?.title = keywordData?.title.query + } + + @IBAction func backButtonItem(_ sender: UIBarButtonItem) { + self.navigationController?.popViewController(animated: true) + } + + private func registerXIB() { + let graphNib = UINib(nibName: graphCellIdentifier, bundle: nil) + tableView.register(graphNib, forCellReuseIdentifier: graphCellIdentifier) + let headerNib = UINib(nibName: headerCellIdentifier, bundle: nil) + tableView.register(headerNib, forCellReuseIdentifier: headerCellIdentifier) + let articleNib = UINib(nibName: articleCellIdentifier, bundle: nil) + tableView.register(articleNib, forCellReuseIdentifier: articleCellIdentifier) + } + + private func tableViewSetup() { + tableView.delegate = self + tableView.dataSource = self + tableView.separatorInset = UIEdgeInsets( + top: 0, + left: UIScreen.main.bounds.width, + bottom: 0, + right: 0 + ) + } + + private func loadGraphData(to keyword: String, _ startDate: String, _ endDate: String) { + let keywordGroups: [[String: Any]] = [[ + "groupName": keyword, + "keywords": [keyword] + ]] + APIManager.requestGraphData( + startDate: startDate, + endDate: endDate, + timeUnit: timeUnit, + keywordGroups: keywordGroups + ) { [weak self] (result) in + guard let self = self else { return } + switch result { + case .success(let graphData): + self.graphData = graphData + case .failure(let error): + print(error.localizedDescription) + } + } + } +} + +extension KeywordDetailViewController: UITableViewDataSource, UITableViewDelegate { + func numberOfSections(in tableView: UITableView) -> Int { + if articleData != nil { + return 2 + } + return 1 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch section { + case 0: + return 1 + default: + if let articleData = articleData { + return articleData.count + } + return 0 + } + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + guard + let headerCell = tableView.dequeueReusableCell( + withIdentifier: headerCellIdentifier + ) as? TrendListHeaderCell else { + return UIView() + } + headerCell.headerLabel.font = UIFont(name: appleGothicNeoBold, size: 17) + switch section { + case 0: + headerCell.headerLabel.text = HeaderTitles.changeInteresting.rawValue + default: + headerCell.headerLabel.text = HeaderTitles.relatedArticles.rawValue + } + return headerCell.contentView + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return 50 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch indexPath.section { + case 0: + guard + let cell = tableView.dequeueReusableCell( + withIdentifier: graphCellIdentifier, + for: indexPath + ) as? KeywordDetailGraphCell else { + return UITableViewCell() + } + cell.graphData = graphData?.results[0] + return cell + default: + guard + let cell = tableView.dequeueReusableCell( + withIdentifier: articleCellIdentifier, + for: indexPath + ) as? KeywordDetailArticleCell else { + return UITableViewCell() + } + if let articleData = articleData { + cell.configure(articleData[indexPath.row]) + } + return cell + } + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return UITableView.automaticDimension + } +} diff --git a/tree/tree/Controller/Live/LiveViewController.swift b/tree/tree/Controller/Live/LiveViewController.swift new file mode 100644 index 0000000..6446d61 --- /dev/null +++ b/tree/tree/Controller/Live/LiveViewController.swift @@ -0,0 +1,123 @@ +// +// LiveViewController.swift +// tree +// +// Created by ParkSungJoon on 29/01/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +class LiveViewController: UIViewController { + + @IBOutlet weak var scrollView: UIScrollView! + @IBOutlet weak var pageControl: UIPageControl! + + private var livePagerPages: [TrendPageView] = [] + private var googleTrendData: TrendDays? { + didSet { + DispatchQueue.main.async { + self.setTrandPages() + } + } + } + private var countryName: String = Country.usa.info.name + private let pageNibName = "TrendPageView" + private let localizedLanguage = LocalizedLanguages.korean.rawValue + + override func viewDidLoad() { + super.viewDidLoad() + scrollView.contentInsetAdjustmentBehavior = .never + networkWithServer(Country.usa.info.code) + NotificationCenter.default.addObserver( + self, + selector: #selector(receiveCountryInfo(_:)), + name: .observeCountryChanging, + object: nil + ) + } + + @objc func receiveCountryInfo(_ notification: Notification) { + guard let countryInfo = notification.userInfo as? [String: String] else { return } + guard + let countryCode = countryInfo["code"], + let countryName = countryInfo["name"] else { + return + } + networkWithServer(countryCode) + self.countryName = countryName + } + + private func setTrandPages() { + livePagerPages = createPages() + setPageScrollView(pages: livePagerPages) + pageControl.numberOfPages = livePagerPages.count + pageControl.currentPage = 0 + view.bringSubviewToFront(pageControl) + } + + private func networkWithServer(_ geo: String) { + APIManager.fetchDailyTrends(hl: localizedLanguage, geo: geo) { [weak self] (result) in + guard let self = self else { return } + switch result { + case .success(let trandData): + self.googleTrendData = trandData + case .failure(let error): + print(error.localizedDescription) + } + } + } + + private func createPages() -> [TrendPageView] { + guard let pageByDays: TrendPageView = Bundle.main.loadNibNamed( + pageNibName, + owner: self, + options: nil + )?.first as? TrendPageView else { + return [] + } + pageByDays.daysKeywordChart = HeaderCellContent( + title: "κΈ‰μƒμŠΉ 검색어", + country: countryName + ) + pageByDays.googleTrendData = googleTrendData + pageByDays.delegate = self + return [pageByDays] + } + + private func setPageScrollView(pages: [TrendPageView]) { + scrollView.frame = CGRect( + x: 0, + y: 0, + width: view.frame.width, + height: view.frame.height + ) + scrollView.contentSize = CGSize( + width: view.frame.width * CGFloat(pages.count), + height: view.frame.height + ) + scrollView.isPagingEnabled = true + for index in 0 ..< pages.count { + pages[index].frame = CGRect( + x: view.frame.width * CGFloat(index), + y: 0, + width: view.frame.width, + height: view.frame.height + ) + scrollView.addSubview(pages[index]) + } + } +} + +extension LiveViewController: PushViewControllerDelegate { + func pushViewControllerWhenDidSelectRow(with rowData: TrendingSearch) { + guard + let detailViewController = self.storyboard?.instantiateViewController( + withIdentifier: "KeywordDetailViewController" + ) as? KeywordDetailViewController else { + return + } + detailViewController.keywordData = rowData + self.navigationController?.pushViewController(detailViewController, animated: true) + } +} diff --git a/tree/tree/Controller/Scrap/ScrapFilterViewController.swift b/tree/tree/Controller/Scrap/ScrapFilterViewController.swift new file mode 100644 index 0000000..bb815da --- /dev/null +++ b/tree/tree/Controller/Scrap/ScrapFilterViewController.swift @@ -0,0 +1,64 @@ +// +// ScrapFilterViewController.swift +// tree +// +// Created by Hyeontae on 05/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +class ScrapFilterViewController: UIViewController { + + @IBOutlet weak var headerView: UIView! + @IBOutlet weak var headerTitleLabel: UILabel! + @IBOutlet weak var tableView: UITableView! + + private let cellIdentifier = "ScrapFilterTableViewCell" + private lazy var categories: [ArticleCategory] = { + ArticleCategory.allCases.filter({ + if $0 == .all { return true } + return ScrapManager.countArticle(category: $0, nil) != 0 + }) + }() + weak var filterDelegate: ScrapFilterDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + tableViewSetup() + } + + private func tableViewSetup() { + tableView.delegate = self + tableView.dataSource = self + tableView.register( + UINib(nibName: cellIdentifier, bundle: nil), + forCellReuseIdentifier: cellIdentifier) + tableView.separatorStyle = .none + } +} + +extension ScrapFilterViewController: UITableViewDataSource, UITableViewDelegate { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return categories.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = + tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) + as? ScrapFilterTableViewCell else { + return UITableViewCell() + } + if indexPath.row == 0 { + cell.setAllCategory(width: view.bounds.width - 32) + } else { + cell.configure(categories[indexPath.row], width: view.bounds.width - 32) + } + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + filterDelegate?.filterArticles(categories[indexPath.row]) + dismiss(animated: true) + } +} diff --git a/tree/tree/Controller/Scrap/ScrapTabBarController.swift b/tree/tree/Controller/Scrap/ScrapTabBarController.swift new file mode 100644 index 0000000..738797b --- /dev/null +++ b/tree/tree/Controller/Scrap/ScrapTabBarController.swift @@ -0,0 +1,40 @@ +// +// ScrapTabBarController.swift +// tree +// +// Created by Hyeontae on 04/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +class ScrapTabBarController: UITabBarController { + + private var bounceAnimation: CAKeyframeAnimation = { + let bounceAnimation = CAKeyframeAnimation(keyPath: "transform.scale") + bounceAnimation.values = [1.0, 1.2, 0.8, 1.2, 1.0] + bounceAnimation.duration = 0.3 + bounceAnimation.calculationMode = CAAnimationCalculationMode.cubic + return bounceAnimation + }() + + override func viewDidLoad() { + super.viewDidLoad() + } + +} + +extension ScrapTabBarController: UITabBarControllerDelegate { + override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { + guard + let index = tabBar.items?.index(of: item), + let imageView = tabBar.subviews[index + 1] + .subviews + .compactMap({$0 as? UIImageView}) + .first + else { + return + } + imageView.layer.add(bounceAnimation, forKey: nil) + } +} diff --git a/tree/tree/Controller/Scrap/ScrapViewController.swift b/tree/tree/Controller/Scrap/ScrapViewController.swift new file mode 100644 index 0000000..676852a --- /dev/null +++ b/tree/tree/Controller/Scrap/ScrapViewController.swift @@ -0,0 +1,76 @@ +// +// ScrapViewController.swift +// tree +// +// Created by Hyeontae on 30/01/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit +import CoreData + +class ScrapViewController: UIViewController { + + @IBOutlet weak var tableView: UITableView! + @IBOutlet weak var filterButton: UIButton! + + private lazy var scrappedArticles: [ScrappedArticle] = { + ScrapManager.fetchArticles() + }() + + override func viewDidLoad() { + super.viewDidLoad() + tableViewSetup() + filterButtonSetup() + } + + private func tableViewSetup() { + tableView.delegate = self + tableView.dataSource = self + } + + private func filterButtonSetup() { + filterButton.addTarget( + self, + action: #selector(filterButtonDidTap), + for: .touchUpInside) + } + + @objc func filterButtonDidTap(_ sender: UIButton) { + guard let tempUIViewController = + UIStoryboard.init(name: "ScrapFilter", bundle: nil) + .instantiateViewController(withIdentifier: "ScrapFilterViewController") + as? ScrapFilterViewController else { + return + } + tempUIViewController.filterDelegate = self + present(tempUIViewController, animated: true) + } + + private func fetchAndReload(selectedCategory category: ArticleCategory) { + if category == .all { + scrappedArticles = ScrapManager.fetchArticles() + } else { + scrappedArticles = ScrapManager.fetchArticles(category) + } + tableView.reloadData() + } +} + +extension ScrapViewController: UITableViewDataSource, UITableViewDelegate { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return scrappedArticles.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) + cell.textLabel?.text = scrappedArticles[indexPath.row].articleTitle + return cell + } +} + +extension ScrapViewController: ScrapFilterDelegate { + func filterArticles(_ article: ArticleCategory) { + fetchAndReload(selectedCategory: article) + } +} diff --git a/tree/tree/Controller/Search/ArticleDetailViewController.swift b/tree/tree/Controller/Search/ArticleDetailViewController.swift index 20ca716..d0330fc 100644 --- a/tree/tree/Controller/Search/ArticleDetailViewController.swift +++ b/tree/tree/Controller/Search/ArticleDetailViewController.swift @@ -13,35 +13,59 @@ class ArticleDetailViewController: UIViewController { @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var dateLabel: UILabel! @IBOutlet weak var authorLabel: UILabel! - @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var imageView: ArticleImage! @IBOutlet weak var contentLabel: UILabel! + private lazy var papagoButton = UIButton(type: .custom) private var floatingButton = UIButton() + private var floatingCheckAnimation: Bool = true + var articleDetail: Article? override func viewDidLoad() { super.viewDidLoad() registerGestureRecognizer() + setArticleData() } override func viewWillAppear(_ animated: Bool) { - createFloatingButton() +// createFloatingButton() self.tabBarController?.tabBar.isHidden = true } override func viewWillDisappear(_ animated: Bool) { - removeFloatingButton() +// removeFloatingButton() self.tabBarController?.tabBar.isHidden = false } + private func getImageFromCache(from articleUrl: String?) { + guard let imageUrl = articleUrl else { + imageView.isHidden = true + return + } + self.imageView.loadImageUrl(articleUrl: imageUrl) + } + private func registerGestureRecognizer() { - imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imageTapped))) + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(imageTapped)) + imageView.addGestureRecognizer(tapGesture) + } + + private func setArticleData() { + titleLabel.text = articleDetail?.title + dateLabel.text = articleDetail?.date + contentLabel.text = articleDetail?.body + getImageFromCache(from: articleDetail?.image ?? nil) + if articleDetail?.author?.isEmpty == false { + if let author = articleDetail?.author?[0].name { + self.authorLabel.text = author + } + } } private func createFloatingButton() { - floatingButton = UIButton(type: .custom) floatingButton.backgroundColor = .black floatingButton.translatesAutoresizingMaskIntoConstraints = false - floatingButton.addTarget(self, action: #selector(translateButtonOnClick), for: .touchUpInside) + floatingButton.addTarget(self, action: #selector(floatingButtonClick), for: .touchUpInside) DispatchQueue.main.async { if let keyWindow = UIApplication.shared.keyWindow { keyWindow.addSubview(self.floatingButton) @@ -55,15 +79,44 @@ class ArticleDetailViewController: UIViewController { } private func removeFloatingButton() { - if floatingButton.superview != nil { + if floatingButton.superview != nil || papagoButton.superview != nil { DispatchQueue.main.async { self.floatingButton.removeFromSuperview() + self.papagoButton.removeFromSuperview() } } } - - @objc private func translateButtonOnClick() { - //reload view + + @objc private func floatingButtonClick() { + UIView.animate(withDuration: 0.5, animations: { + self.floatingButton.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) + }) { (_) in + if self.floatingCheckAnimation { + UIView.animate(withDuration: 0.2, animations: { + self.floatingButton.transform = CGAffineTransform.identity + self.papagoButton.backgroundColor = .black + self.papagoButton.translatesAutoresizingMaskIntoConstraints = false + self.papagoButton.addTarget(self, action: #selector(self.papagoTranslate), for: .touchUpInside) + DispatchQueue.main.async { + if let keyWindow = UIApplication.shared.keyWindow { + keyWindow.addSubview(self.papagoButton) + NSLayoutConstraint.activate([ + keyWindow.trailingAnchor.constraint(equalTo: self.papagoButton.trailingAnchor, constant: 25), + self.floatingButton.topAnchor.constraint(equalTo: self.papagoButton.bottomAnchor, constant: 16), + self.papagoButton.widthAnchor.constraint(equalToConstant: 30), + self.papagoButton.heightAnchor.constraint(equalToConstant: 30)]) + } + self.papagoButton.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) + } + }) + } else { + UIView.animate(withDuration: 0.2, animations: { + self.floatingButton.transform = CGAffineTransform.identity + }) + self.papagoButton.removeFromSuperview() + } + self.floatingCheckAnimation.toggle() + } } @objc private func imageTapped() { @@ -72,4 +125,9 @@ class ArticleDetailViewController: UIViewController { articleViewer.articleImage = articleImage self.present(articleViewer, animated: false, completion: nil) } + + @objc private func papagoTranslate() { + + } + } diff --git a/tree/tree/Controller/Search/ArticleImageViewController.swift b/tree/tree/Controller/Search/ArticleImageViewController.swift index 3bb4323..237fca4 100644 --- a/tree/tree/Controller/Search/ArticleImageViewController.swift +++ b/tree/tree/Controller/Search/ArticleImageViewController.swift @@ -13,8 +13,8 @@ class ArticleImageViewController: UIViewController { @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var scrollView: UIScrollView! - var articleImage: UIImage? private var initTouchPosition: CGPoint = CGPoint(x: 0, y: 0) + var articleImage: UIImage? override func viewDidLoad() { super.viewDidLoad() @@ -52,7 +52,7 @@ class ArticleImageViewController: UIViewController { self.view.frame = CGRect(x: 0, y: touchPosition.y - initTouchPosition.y, width: self.view.frame.size.width, height: self.view.frame.size.height) } } else if sender.state == UIGestureRecognizer.State.ended || sender.state == UIGestureRecognizer.State.cancelled { - if touchPosition.y - initTouchPosition.y > 100 { + if touchPosition.y - initTouchPosition.y > 150 { self.dismiss(animated: true, completion: nil) } else { UIView.animate(withDuration: 0.5, animations: { diff --git a/tree/tree/Controller/Search/SearchFilterViewController.swift b/tree/tree/Controller/Search/SearchFilterViewController.swift new file mode 100644 index 0000000..09af333 --- /dev/null +++ b/tree/tree/Controller/Search/SearchFilterViewController.swift @@ -0,0 +1,126 @@ +// +// SearchFilterViewController.swift +// tree +// +// Created by Hyeontae on 28/01/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +class SearchFilterViewController: UIViewController { + + @IBOutlet weak var languageStackView: UIStackView! + @IBOutlet weak var categoryStackView: UIStackView! + @IBOutlet weak var languageLabel: UILabel! + @IBOutlet weak var categoryLabel: UILabel! + @IBOutlet weak var selectPickViewer: PickerView! + @IBOutlet weak var pickerView: UIStackView! + @IBOutlet weak var saveButton: UIButton! + @IBOutlet weak var keywordSortStackView: UIStackView! + @IBOutlet weak var keywordSegmentedControl: UISegmentedControl! + @IBOutlet weak var sortSegmentedControl: UISegmentedControl! + @IBOutlet var collectionOfSegmentedControl: [UISegmentedControl]? + + private var selectViewIsPresented: Bool = false + var filterValue: [String: String]? + weak var settingDelegate: FilterSettingDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + settingSegment() + registerDelegate() + settingFilterValue() + roundConersSetup() + } + + private func registerDelegate() { + selectPickViewer.delegate = self + selectPickViewer.dataSource = self + } + + private func settingFilterValue() { + categoryLabel.text = filterValue?["category"] + languageLabel.text = filterValue?["language"] + if filterValue?["keyword"] == "Body" { + keywordSegmentedControl.selectedSegmentIndex = 1 + } + if filterValue?["sort"] == "Relevance" { + sortSegmentedControl.selectedSegmentIndex = 1 + } + } + + private func settingSegment() { + pickerView.isHidden = true + let font = UIFont(name: "AppleSDGothicNeo-Bold", size: 16) + let normalAttributedString = [ NSAttributedString.Key.font: font as Any, NSAttributedString.Key.foregroundColor: UIColor.gray ] + let selectedAttributedString = [ NSAttributedString.Key.font: font as Any, NSAttributedString.Key.foregroundColor: UIColor.black ] + collectionOfSegmentedControl?.forEach({ + $0.backgroundColor = .clear + $0.tintColor = .lightGray + $0.setTitleTextAttributes(normalAttributedString, for: .normal) + $0.setTitleTextAttributes(selectedAttributedString, for: .selected) + }) + } + + private func roundConersSetup() { + saveButton.roundCorners(layer: saveButton.layer, radius: CGFloat(5)) + view.roundCorners(layer: self.view.layer, radius: CGFloat(15)) + } + + private func makeHidden(isPresented: Bool) { + languageStackView.isHidden = isPresented + categoryStackView.isHidden = !isPresented + } + + @IBAction func selectButtonClick(_ sender: UIButton) { + selectViewIsPresented.toggle() + selectPickViewer.tagNumber = sender.tag + if selectViewIsPresented { + selectPickViewer.tagNumber == 0 ? + makeHidden(isPresented: selectViewIsPresented) : + makeHidden(isPresented: !selectViewIsPresented) + } else { + languageStackView.isHidden = selectViewIsPresented + categoryStackView.isHidden = selectViewIsPresented + } + UIView.animate(withDuration: 0.3) { + self.keywordSortStackView.isHidden = self.selectViewIsPresented + self.pickerView.isHidden = !self.selectViewIsPresented + } + } + + @IBAction func saveButtonClick(_ sender: Any) { + if let keyword = keywordSegmentedControl.titleForSegment(at: keywordSegmentedControl.selectedSegmentIndex), + let sort = sortSegmentedControl.titleForSegment(at: sortSegmentedControl.selectedSegmentIndex), + let category = categoryLabel.text, + let language = languageLabel.text { + settingDelegate?.observeUserSetting(keyword: keyword, sort: sort, category: category, language: language) + } + self.dismiss(animated: true, completion: nil) + } +} + +extension SearchFilterViewController: UIPickerViewDelegate, UIPickerViewDataSource { + + func numberOfComponents(in pickerView: UIPickerView) -> Int { + return 1 + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + return selectPickViewer.getList().count + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + let selectValue = selectPickViewer.getList()[row] + if selectPickViewer.tagNumber == 0 { + categoryLabel.text = selectValue + } else { + languageLabel.text = selectValue + } + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + return selectPickViewer.getList()[row] + } +} diff --git a/tree/tree/Controller/Search/SearchViewController.swift b/tree/tree/Controller/Search/SearchViewController.swift index 0c56bfc..46b228d 100644 --- a/tree/tree/Controller/Search/SearchViewController.swift +++ b/tree/tree/Controller/Search/SearchViewController.swift @@ -13,14 +13,26 @@ class SearchViewController: UIViewController { @IBOutlet weak var uiSearchBarOuterView: UIView! @IBOutlet weak var uiSearchBar: UISearchBar! @IBOutlet weak var uiTableView: UITableView! - @IBOutlet weak var navigationFilterItem: UIBarItem! + @IBOutlet weak var navigationFilterItem: UIButton! private let cellIdentifier: String = "ArticleFeedTableViewCell" + private let articleImage: ArticleImage = ArticleImage() + private var loadingView: LoadingView? + private var defaultView: DefaultLabelView? private var topOffset: CGFloat = UIApplication.shared.statusBarOrientation.isLandscape ? 44 : 64 private var tableViewContentOffsetY: CGFloat = 0 private var tableViewScrollCount: (down: Int, up: Int) = (0, 0) private var searchBarTextField: UITextField? private var searchBarIsPresented: Bool = true + private var transitionManager = PresentationManager() + private var articles: [Article]? + private var defaultLabel: UILabel = UILabel() + private var searchKeyword: String = "" + private var page: Int = 1 + private var totalPage: Int = 0 + private var isMoreLoading: Bool = false + private var isPresentedCheck: Bool = true + private lazy var searchFilter = [String: String]() override func viewDidLoad() { super.viewDidLoad() @@ -29,101 +41,240 @@ class SearchViewController: UIViewController { tableViewSetting() navigationBarSetting() registerArticleCell() + filterItemSetting() + userFilter() + setDefaultView(message: "Please Search πŸ”Ž") } - func delegateSetting() { + override func viewWillDisappear(_ animated: Bool) { + isPresentedCheck = searchBarIsPresented + } + + private func setLoadingView() { + let loadingViewFrame = CGRect(x: 0, y: 0, width: 100, height: 100) + loadingView = LoadingView(frame: loadingViewFrame) + guard let loadView = loadingView else { return } + loadView.center = self.view.center + self.view.addSubview(loadView) + } + + private func setDefaultView(message: String) { + let defaultViewFrame = CGRect(x: 0, + y: 0, + width: self.view.frame.width, + height: 150) + defaultView = DefaultLabelView(frame: defaultViewFrame) + guard let defaultView = defaultView else { return } + defaultView.defaultMessage.text = message + defaultView.center = self.view.center + self.view.addSubview(defaultView) + } + + private func delegateSetting() { uiSearchBar.delegate = self uiTableView.delegate = self uiTableView.dataSource = self + uiTableView.prefetchDataSource = self } - func searchBarSetting() { + private func searchBarSetting() { uiSearchBar.backgroundImage = UIImage() guard let searchBarTextfield: UITextField = uiSearchBar.value(forKey: "searchField") as? UITextField else { return } searchBarTextfield.backgroundColor = UIColor.lightGray searchBarTextfield.textColor = UIColor.black } - func tableViewSetting() { + private func tableViewSetting() { uiTableView.contentInset = UIEdgeInsets(top: topOffset, left: 0, bottom: 0, right: 0) uiTableView.separatorStyle = .none } - func navigationBarSetting() { + private func navigationBarSetting() { guard let navigationBar = self.navigationController?.navigationBar else { return } navigationBar.backgroundColor = UIColor.white navigationBar.setBackgroundImage(UIImage(), for: .any, barMetrics: .default) navigationBar.shadowImage = UIImage() } - func registerArticleCell() { + private func registerArticleCell() { let articleFeedNib = UINib(nibName: "ArticleFeedTableViewCell", bundle: nil) uiTableView.register(articleFeedNib, forCellReuseIdentifier: cellIdentifier) } + + private func setMessageBySearchState(to message: String) { + defaultLabel.text = message + defaultLabel.frame.size = CGSize(width: 200, height: 50) + defaultLabel.center = self.view.center + defaultLabel.textAlignment = .center + view.addSubview(defaultLabel) + } + + private func checkFilterStatus(using searchFilter: [String: String], type: ArticleType) { + guard let keyword = searchFilter["keyword"], + let language = searchFilter["language"], + let sort = searchFilter["sort"] + else { return } + switch type { + case .load: + loadArticles(keyword: keyword, language: language, sort: sort) + case .loadMore: + loadMoreArticles(keyword: keyword, language: language, sort: sort) + } + } + + private func loadArticles(keyword: String, + language: String, + sort: String) { + articles = nil + self.defaultView?.removeFromSuperview() + self.uiTableView.reloadData() + self.setLoadingView() + APIManager.fetchArticles( + keyword: searchKeyword, + keywordLoc: keyword, + lang: language, + articlesSortBy: sort, + articlesPage: 1 + ) { (result) in + switch result { + case .success(let articleData): + self.articles = articleData.articles.results + self.totalPage = articleData.articles.pages + DispatchQueue.main.async { + self.uiTableView.reloadData() + self.loadingView?.removeFromSuperview() + if self.articles?.count == 0 { + self.setDefaultView(message: "No Results πŸ”Ž") + } + } + case .failure(let error): + print(error.localizedDescription) + } + } + } + + private func loadMoreArticles( + keyword: String, + language: String, + sort: String) { + if page >= totalPage { return } + page += 1 + APIManager.fetchArticles( + keyword: keyword, + keywordLoc: keyword, + lang: language, + articlesSortBy: sort, + articlesPage: page + ) { (result) in + switch result { + case .success(let articleData): + self.articles?.append(contentsOf: articleData.articles.results) + DispatchQueue.main.async { + self.uiTableView.reloadData() + self.isMoreLoading = false + } + case .failure(let error): + print(error.localizedDescription) + } + } + } + + private func filterItemSetting() { + navigationFilterItem.addTarget(self, action: #selector(filterItemTapAtion), for: .touchUpInside) + } + + @objc private func filterItemTapAtion() { + guard + let filterViewController = self.storyboard?.instantiateViewController(withIdentifier: "SearchFilterViewController") as? SearchFilterViewController else { return } + filterViewController.settingDelegate = self + filterViewController.filterValue = searchFilter + filterViewController.transitioningDelegate = transitionManager + filterViewController.modalPresentationStyle = .custom + present(filterViewController, animated: true) + } + } +// MARK: TableView extension SearchViewController: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 30 + return articles?.count ?? 0 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? ArticleFeedTableViewCell else { - return UITableViewCell() - } + guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? ArticleFeedTableViewCell else { return UITableViewCell() } + guard let article = articles?[indexPath.row] else { return UITableViewCell() } + cell.settingData(article: article) return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let storyboard = UIStoryboard(name: "ArticleDetail", bundle: nil) - let articleView = storyboard.instantiateViewController(withIdentifier: "ArticleDetailViewController") + guard let articleView = storyboard.instantiateViewController(withIdentifier: "ArticleDetailViewController") as? ArticleDetailViewController else { return } + articleView.articleDetail = articles?[indexPath.row] self.navigationController?.pushViewController(articleView, animated: true) } - +} + +// MARK: ScrollView +extension SearchViewController { func scrollViewDidScroll(_ scrollView: UIScrollView) { - if tableViewContentOffsetY < scrollView.contentOffset.y { + if !isMoreLoading { + let scrollPosition = scrollView.contentSize.height - scrollView.frame.size.height - scrollView.contentOffset.y + if scrollPosition > 0 && scrollPosition < scrollView.contentSize.height * 0.3 { + checkFilterStatus(using: searchFilter, type: ArticleType.loadMore) + isMoreLoading.toggle() + } + } + if searchBarIsPresented && tableViewContentOffsetY < scrollView.contentOffset.y { scrollViewCheckCount(.down) - } else { + } else if !searchBarIsPresented && tableViewContentOffsetY > scrollView.contentOffset.y { scrollViewCheckCount(.up) - } + } tableViewContentOffsetY = scrollView.contentOffset.y } func scrollViewCheckCount(_ scrollDirection: ScrollDirection) { - let directionIsDown: Bool = scrollDirection == .down ? true : false + let directionIsDown = scrollDirection == .down ? true : false tableViewScrollCount.down += directionIsDown == true ? 1 : 0 tableViewScrollCount.up += directionIsDown == true ? 0 : 1 - if tableViewScrollCount.down > 15 || tableViewScrollCount.up > 15 { + if tableViewScrollCount.down > 15 || tableViewScrollCount.up > 5 { scrollSettingFunction(directionIsDown ? .down : .up) } } func scrollSettingFunction(_ direction: ScrollDirection) { switch direction { - case .up where !searchBarIsPresented: + case .up: searchBarIsPresented = true searchBarShowAndHideAnimation(.up) - case .down where searchBarIsPresented: + case .down: searchBarIsPresented = false searchBarShowAndHideAnimation(.down) - default: - break } tableViewScrollCount = (0, 0) } func searchBarShowAndHideAnimation(_ direction: ScrollDirection) { let directionIsDown = direction == .down ? true : false - UIView.animate(withDuration: 0.5) { [weak self] in - guard let self = self else { return } - self.uiSearchBarOuterView.center.y += directionIsDown ? (-1) * self.topOffset : self.topOffset + if !isPresentedCheck { self.uiTableView.contentInset.top = directionIsDown ? 0 : self.topOffset - self.uiSearchBarOuterView.alpha = directionIsDown ? 0 : 1.0 + UIView.animate(withDuration: 0.3) { + self.uiSearchBarOuterView.alpha = 1 + self.isPresentedCheck.toggle() + } + } else { + UIView.animate(withDuration: 0.5) { + self.uiSearchBarOuterView.center.y += directionIsDown ? (-1) * self.topOffset : self.topOffset + self.uiTableView.contentInset.top = directionIsDown ? 0 : self.topOffset + self.uiSearchBarOuterView.alpha = directionIsDown ? 0 : 1.0 + } } } } +// MARK: SearchBar extension SearchViewController: UISearchBarDelegate { func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { uiSearchBar.setShowsCancelButton(true, animated: true) @@ -131,6 +282,11 @@ extension SearchViewController: UISearchBarDelegate { func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { self.navigationItem.title = searchBar.text ?? "Search" + if let getSearchKeyword = searchBar.text { + searchKeyword = getSearchKeyword + checkFilterStatus(using: searchFilter, type: ArticleType.load) + defaultLabel.removeFromSuperview() + } searchBarHideAndSetting() } @@ -144,3 +300,63 @@ extension SearchViewController: UISearchBarDelegate { uiSearchBar.resignFirstResponder() } } + +// MARK: Prefetch +extension SearchViewController: UITableViewDataSourcePrefetching { + func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { + indexPaths.forEach({ + guard let articleUrl = articles?[$0.row].image else { return } + articleImage.loadImageUrl(articleUrl: articleUrl) + }) + } + + func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { + indexPaths.forEach({ + guard let articleUrl = articles?[$0.row].image else { return } + articleImage.cancleLoadingImage(articleUrl) + }) + } +} + +// MARK: Filter Delegate +extension SearchViewController: FilterSettingDelegate { + func observeUserSetting( + keyword: String, + sort: String, + category: String, + language: String + ) { + updateUserFilter( + keyword: keyword, + sort: sort, + category: category, + language: language + ) + UserDefaults.standard.set(searchFilter, forKey: "searchFilter") + } + + private func updateUserFilter( + keyword: String, + sort: String, + category: String, + language: String + ) { + searchFilter.updateValue(keyword, forKey: "keyword") + searchFilter.updateValue(sort, forKey: "sort") + searchFilter.updateValue(category, forKey: "category") + searchFilter.updateValue(language.lowercased(), forKey: "language") + } + + private func userFilter() { + guard + let userFilter = UserDefaults.standard.dictionary(forKey: "searchFilter") else { + return + } + if let keyword = userFilter["keyword"] as? String, + let sort = userFilter["sort"] as? String, + let category = userFilter["category"] as? String, + let language = userFilter["language"] as? String { + updateUserFilter(keyword: keyword, sort: sort, category: category, language: language) + } + } +} diff --git a/tree/tree/Extension/Extension+ImageView.swift b/tree/tree/Extension/Extension+ImageView.swift new file mode 100644 index 0000000..704bebf --- /dev/null +++ b/tree/tree/Extension/Extension+ImageView.swift @@ -0,0 +1,60 @@ +// +// Extension+ImageView.swift +// tree +// +// Created by hyeri kim on 30/01/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +class ArticleImage: UIImageView { + private let imageCache = ImageCache() + private let ioQueue = DispatchQueue(label: "diskCache") + private var task = [URLSessionTask]() + private var imageUrl: String? + + func cancleLoadingImage(_ articleUrl: String) { + guard let imageURL = URL(string: articleUrl) else { return } + guard let taskIndex = task.index(where: { $0.originalRequest?.url == imageURL}) else { return } + let myTask = task[taskIndex] + myTask.cancel() + task.remove(at: taskIndex) + } + + func loadImageUrl(articleUrl: String) { + imageUrl = articleUrl + image = nil + let extract = self.imageUrl?.components(separatedBy: "/").last + + if let imageFromCache = imageCache.memoryCache.object(forKey: articleUrl as AnyObject) as? UIImage { + self.image = imageFromCache + return + } else { + if let imagePath = imageCache.path(for: extract ?? ""), let imageToDisk = UIImage(contentsOfFile: imagePath.path) { + self.image = imageToDisk + self.imageCache.memoryCache.setObject(imageToDisk, forKey: articleUrl as AnyObject) + return + } + } + guard let imageURL = URL(string: articleUrl) else { return } + guard task.index(where: {$0.originalRequest?.url == imageURL }) == nil else { return } + let myTask = URLSession.shared.dataTask(with: imageURL) { [weak self] (data, _, _) in + DispatchQueue.main.async { + guard let self = self else { return } + guard let data = data else { return } + guard let imageToCache = UIImage(data: data) else { return } + if self.imageUrl == articleUrl { + self.image = imageToCache + } + self.ioQueue.async { + if let path = extract { + try? self.imageCache.store(image: imageToCache, name: path) + } + } + } + } + myTask.resume() + task.append(myTask) + } +} diff --git a/tree/tree/Extension/Extension+Notification.swift b/tree/tree/Extension/Extension+Notification.swift new file mode 100644 index 0000000..9dbabf5 --- /dev/null +++ b/tree/tree/Extension/Extension+Notification.swift @@ -0,0 +1,13 @@ +// +// Extension+Notification.swift +// tree +// +// Created by ParkSungJoon on 04/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import Foundation + +extension Notification.Name { + static let observeCountryChanging = Notification.Name("observeCountryChanging") +} diff --git a/tree/tree/Extension/Extension+PickerView.swift b/tree/tree/Extension/Extension+PickerView.swift new file mode 100644 index 0000000..c730193 --- /dev/null +++ b/tree/tree/Extension/Extension+PickerView.swift @@ -0,0 +1,23 @@ +// +// Extension+PickerView.swift +// tree +// +// Created by hyeri kim on 04/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +class PickerView: UIPickerView { + private let categories = ["Arts","Businees","Computers","Games","Health","Home", "Recreation","Reference","Regional","Science","Shopping","Society","Sports"] + private let languages = ["Any","Eng","Jpn","Fra","Ita","Spa","Rus","Deu"] + var tagNumber = 0 + + func getList() -> [String] { + if tagNumber == 0 { + return categories + } else { + return languages + } + } +} diff --git a/tree/tree/Extension/Extension+UIColor.swift b/tree/tree/Extension/Extension+UIColor.swift index 9106bbf..83df10a 100644 --- a/tree/tree/Extension/Extension+UIColor.swift +++ b/tree/tree/Extension/Extension+UIColor.swift @@ -9,7 +9,33 @@ import UIKit extension UIColor { + public convenience init(hexString: String) { + let red, green, blue: CGFloat + if hexString.hasPrefix("#") { + let start = hexString.index(hexString.startIndex, offsetBy: 1) + let hexColor = String(hexString[start...]) + if hexColor.count == 6 { + let scanner = Scanner(string: hexColor) + var hexNumber: UInt64 = 0 + if scanner.scanHexInt64(&hexNumber) { + red = CGFloat((hexNumber & 0xff0000) >> 16) / 255 + green = CGFloat((hexNumber & 0x00ff00) >> 8) / 255 + blue = CGFloat((hexNumber & 0x0000ff) >> 0) / 255 + self.init(red: red, green: green, blue: blue, alpha: 1.0) + return + } + } + } + self.init(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) + } + class var lightGray: UIColor { - return UIColor(red: 142/256, green: 142/256, blue: 147/256, alpha: 0.12) + return UIColor(red: 142/255.0, green: 142/255.0, blue: 147/255.0, alpha: 0.12) + } + class var brightBlue: UIColor { + return UIColor(red: 34/255.0, green: 105/255.0, blue: 232/255.0, alpha: 1.0) + } + class var whiteGray: UIColor { + return UIColor(red: 205/255.0, green: 205/255.0, blue: 205/255.0, alpha: 1.0) } } diff --git a/tree/tree/Extension/Extension+UIView.swift b/tree/tree/Extension/Extension+UIView.swift new file mode 100644 index 0000000..762ec25 --- /dev/null +++ b/tree/tree/Extension/Extension+UIView.swift @@ -0,0 +1,26 @@ +// +// Extension+UIView.swift +// tree +// +// Created by hyeri kim on 30/01/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +extension UIView { + func applyShadow(shadowView: UIView, width: CGFloat, height: CGFloat) { + let shadowPath = UIBezierPath(roundedRect: shadowView.bounds, cornerRadius: 14.0) + shadowView.layer.masksToBounds = false + shadowView.layer.shadowRadius = 14.0 + shadowView.layer.shadowColor = UIColor.black.cgColor + shadowView.layer.shadowOffset = CGSize(width: width, height: height) + shadowView.layer.shadowOpacity = 0.15 + shadowView.layer.shadowPath = shadowPath.cgPath + } + + func roundCorners(layer targetLayer: CALayer, radius withRaidus: CGFloat) { + targetLayer.cornerRadius = withRaidus + targetLayer.masksToBounds = true + } +} diff --git a/tree/tree/Helper/ArticleTypeEnum.swift b/tree/tree/Helper/ArticleTypeEnum.swift new file mode 100644 index 0000000..f45a266 --- /dev/null +++ b/tree/tree/Helper/ArticleTypeEnum.swift @@ -0,0 +1,14 @@ +// +// ArticleTypeEnum.swift +// tree +// +// Created by hyeri kim on 08/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import Foundation + +enum ArticleType { + case load + case loadMore +} diff --git a/tree/tree/Helper/Enum+Countries.swift b/tree/tree/Helper/Enum+Countries.swift new file mode 100644 index 0000000..0efe3b9 --- /dev/null +++ b/tree/tree/Helper/Enum+Countries.swift @@ -0,0 +1,31 @@ +// +// Enum+Countries.swift +// tree +// +// Created by ParkSungJoon on 03/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +enum Country: Int { + case usa = 0 + case kor = 1 + case jpn = 2 + case fra = 3 + case rus = 4 + case uk = 5 + case ita = 6 + case ger = 7 + + var info: (name: String, code: String) { + switch self { + case .usa: return ("λ―Έκ΅­", "US") + case .kor: return ("λŒ€ν•œλ―Όκ΅­", "KR") + case .jpn: return ("일본", "JP") + case .fra: return ("ν”„λž‘μŠ€", "FR") + case .rus: return ("λŸ¬μ‹œμ•„", "RU") + case .uk: return ("영ꡭ", "GB") + case .ita: return ("μ΄νƒˆλ¦¬μ•„", "IT") + case .ger: return ("독일", "DE") + } + } +} diff --git a/tree/tree/Helper/Enum+HeaderTitles.swift b/tree/tree/Helper/Enum+HeaderTitles.swift new file mode 100644 index 0000000..4c0ed81 --- /dev/null +++ b/tree/tree/Helper/Enum+HeaderTitles.swift @@ -0,0 +1,14 @@ +// +// Enum+HeaderTitles.swift +// tree +// +// Created by ParkSungJoon on 11/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import Foundation + +enum HeaderTitles: String { + case changeInteresting = "μ‹œκ°„ 흐름에 λ”°λ₯Έ 관심도 λ³€ν™”" + case relatedArticles = "관심 기사" +} diff --git a/tree/tree/Helper/Enum+LocalizedLanguages.swift b/tree/tree/Helper/Enum+LocalizedLanguages.swift new file mode 100644 index 0000000..ed81461 --- /dev/null +++ b/tree/tree/Helper/Enum+LocalizedLanguages.swift @@ -0,0 +1,14 @@ +// +// Enum+LocalizedLanguages.swift +// tree +// +// Created by ParkSungJoon on 08/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import Foundation + +enum LocalizedLanguages: String { + case korean = "ko" + case english = "en" +} diff --git a/tree/tree/Helper/Enum+TimeIntervalTypes.swift b/tree/tree/Helper/Enum+TimeIntervalTypes.swift new file mode 100644 index 0000000..68e2702 --- /dev/null +++ b/tree/tree/Helper/Enum+TimeIntervalTypes.swift @@ -0,0 +1,21 @@ +// +// Enum+TimeIntervalTypes.swift +// tree +// +// Created by ParkSungJoon on 08/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import Foundation + +enum TimeIntervalTypes { + case oneDay + case oneMonth + + var value: TimeInterval { + switch self { + case .oneDay: return TimeInterval(86400) + case .oneMonth: return TimeInterval(30 * 86400) + } + } +} diff --git a/tree/tree/Helper/Enum+TimeUnitTypes.swift b/tree/tree/Helper/Enum+TimeUnitTypes.swift new file mode 100644 index 0000000..8f47fc8 --- /dev/null +++ b/tree/tree/Helper/Enum+TimeUnitTypes.swift @@ -0,0 +1,23 @@ +// +// Enum+TimeUnitTypes.swift +// tree +// +// Created by ParkSungJoon on 08/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import Foundation + +enum TimeUnitTypes { + case week + case month + case year + + var value: String { + switch self { + case .week: return "week" + case .month: return "month" + case .year: return "year" + } + } +} diff --git a/tree/tree/Helper/ImageHelper/ImageCache.swift b/tree/tree/Helper/ImageHelper/ImageCache.swift new file mode 100644 index 0000000..2505a77 --- /dev/null +++ b/tree/tree/Helper/ImageHelper/ImageCache.swift @@ -0,0 +1,30 @@ +// +// ImageCache.swift +// tree +// +// Created by hyeri kim on 03/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +open class ImageCache { + let memoryCache = NSCache() + + func store(image: UIImage, name: String) throws { + guard let imageData = image.jpegData(compressionQuality: 1.0) else { return } + guard let imagePath = self.path(for: name) else { return } + if !FileManager.default.fileExists(atPath: imagePath.path) { + do { + try imageData.write(to: imagePath) + } catch let error as NSError { + print("error occured \(error)") + } + } + } + + func path(for imageName: String) -> URL? { + let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first + return directory?.appendingPathComponent(imageName) + } +} diff --git a/tree/tree/Helper/PresentationManager/PresentationController.swift b/tree/tree/Helper/PresentationManager/PresentationController.swift new file mode 100644 index 0000000..4850abf --- /dev/null +++ b/tree/tree/Helper/PresentationManager/PresentationController.swift @@ -0,0 +1,82 @@ +// +// PresentationController.swift +// tree +// +// Created by Hyeontae on 28/01/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +class PresentationController: UIPresentationController { + fileprivate var dimmingView: UIView! + + override var frameOfPresentedViewInContainerView: CGRect { + var frame: CGRect = .zero + guard let parentContainerView = containerView else { return CGRect() } + frame.size = size(forChildContentContainer: presentedViewController, + withParentContainerSize: parentContainerView.bounds.size) + frame.origin.y = parentContainerView.frame.height - 300 + return frame + } + + override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) { + super.init(presentedViewController: presentedViewController, + presenting: presentingViewController) + dimmingViewSetting() + } + + override func presentationTransitionWillBegin() { + containerView?.insertSubview(dimmingView, at: 0) + NSLayoutConstraint.activate( + NSLayoutConstraint.constraints(withVisualFormat: "V:|[dimmingView]|", + options: [], metrics: nil, views: ["dimmingView": dimmingView])) + NSLayoutConstraint.activate( + NSLayoutConstraint.constraints(withVisualFormat: "H:|[dimmingView]|", + options: [], metrics: nil, views: ["dimmingView": dimmingView])) + + guard let coordinator = presentedViewController.transitionCoordinator else { + dimmingView.alpha = 1.0 + return + } + coordinator.animate(alongsideTransition: { _ in + self.dimmingView.alpha = 1.0 + }) + } + + override func dismissalTransitionWillBegin() { + guard let coordinator = presentedViewController.transitionCoordinator else { + dimmingView.alpha = 0.0 + return + } + coordinator.animate(alongsideTransition: { _ in + self.dimmingView.alpha = 0.0 + }) + } + + override func containerViewWillLayoutSubviews() { + presentedView?.frame = frameOfPresentedViewInContainerView + } + + override func size(forChildContentContainer container: UIContentContainer, + withParentContainerSize parentSize: CGSize + ) -> CGSize { + return CGSize(width: parentSize.width, height: 375) + } +} + +private extension PresentationController { + + func dimmingViewSetting() { + dimmingView = UIView() + dimmingView.translatesAutoresizingMaskIntoConstraints = false + dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + dimmingView.alpha = 0.0 + let recognizer = UITapGestureRecognizer(target: self, action: #selector(dimmingViewTapAction(recognizer:))) + dimmingView.addGestureRecognizer(recognizer) + } + + @objc func dimmingViewTapAction(recognizer: UITapGestureRecognizer) { + presentingViewController.dismiss(animated: true) + } +} diff --git a/tree/tree/Helper/PresentationManager/PresentationManager.swift b/tree/tree/Helper/PresentationManager/PresentationManager.swift new file mode 100644 index 0000000..89b2a62 --- /dev/null +++ b/tree/tree/Helper/PresentationManager/PresentationManager.swift @@ -0,0 +1,25 @@ +// +// PresentationManager.swift +// tree +// +// Created by Hyeontae on 28/01/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +class PresentationManager: NSObject { + +} + +extension PresentationManager: UIViewControllerTransitioningDelegate { + func presentationController( + forPresented presented: UIViewController, + presenting: UIViewController?, + source: UIViewController + ) -> UIPresentationController? { + let presentationController = PresentationController(presentedViewController: presented, + presenting: presenting) + return presentationController + } +} diff --git a/tree/tree/Helper/ScrapFilterDelegate.swift b/tree/tree/Helper/ScrapFilterDelegate.swift new file mode 100644 index 0000000..c408be6 --- /dev/null +++ b/tree/tree/Helper/ScrapFilterDelegate.swift @@ -0,0 +1,11 @@ +// +// ScrapFilterDelegate.swift +// tree +// +// Created by Hyeontae on 09/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +protocol ScrapFilterDelegate: class { + func filterArticles(_ article: ArticleCategory) +} diff --git a/tree/tree/Helper/SearchFilterProtocol.swift b/tree/tree/Helper/SearchFilterProtocol.swift new file mode 100644 index 0000000..726cd14 --- /dev/null +++ b/tree/tree/Helper/SearchFilterProtocol.swift @@ -0,0 +1,13 @@ +// +// SearchFilterProtocol.swift +// tree +// +// Created by hyeri kim on 05/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import Foundation + +protocol FilterSettingDelegate: class { + func observeUserSetting(keyword: String, sort: String, category: String, language: String) +} diff --git a/tree/tree/Info.plist b/tree/tree/Info.plist index 0480356..efe72ea 100644 --- a/tree/tree/Info.plist +++ b/tree/tree/Info.plist @@ -2,15 +2,6 @@ - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - LSApplicationCategoryType - - UIViewControllerBasedStatusBarAppearance - CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -27,12 +18,19 @@ 1.0 CFBundleVersion 1 + LSApplicationCategoryType + LSRequiresIPhoneOS + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile - Search + Main UIRequiredDeviceCapabilities armv7 @@ -50,5 +48,7 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIViewControllerBasedStatusBarAppearance + diff --git a/tree/tree/Model/Articles.swift b/tree/tree/Model/Articles.swift index 819cd70..12a4ad0 100644 --- a/tree/tree/Model/Articles.swift +++ b/tree/tree/Model/Articles.swift @@ -13,9 +13,9 @@ struct Articles: Codable { } struct Results: Codable { - let page: Int // Current page number - let totalResults: Int // Total articles count - let pages: Int // Total pages count + let page: Int + let totalResults: Int + let pages: Int let results: [Article] } @@ -31,14 +31,21 @@ struct Article: Codable { let source: Source let author: [Author]? let image: String? + let categories: [Category] private enum CodingKeys: String, CodingKey { - case uri, lang, date, time + case uri, lang, date, time, categories case sim, url, title, body, source, image case author = "authors" } } +struct Category: Codable { + let uri: String + let label: String + let wgt: Int +} + struct Source: Codable { let uri: String let dataType: String diff --git a/tree/tree/Model/CoreDataModel/ScrappedArticle+CoreDataClass.swift b/tree/tree/Model/CoreDataModel/ScrappedArticle+CoreDataClass.swift new file mode 100644 index 0000000..95671ca --- /dev/null +++ b/tree/tree/Model/CoreDataModel/ScrappedArticle+CoreDataClass.swift @@ -0,0 +1,15 @@ +// +// ScrappedArticle+CoreDataClass.swift +// +// +// Created by Hyeontae on 30/01/2019. +// +// + +import Foundation +import CoreData + +@objc(ScrappedArticle) +public class ScrappedArticle: NSManagedObject { + +} diff --git a/tree/tree/Model/CoreDataModel/ScrappedArticle+CoreDataProperties.swift b/tree/tree/Model/CoreDataModel/ScrappedArticle+CoreDataProperties.swift new file mode 100644 index 0000000..7ed00a1 --- /dev/null +++ b/tree/tree/Model/CoreDataModel/ScrappedArticle+CoreDataProperties.swift @@ -0,0 +1,29 @@ +// +// ScrappedArticle+CoreDataProperties.swift +// +// +// Created by Hyeontae on 30/01/2019. +// +// + +import Foundation +import CoreData + +extension ScrappedArticle { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "ScrappedArticle") + } + + @NSManaged public var articleAuthor: String? + @NSManaged public var articleDescription: String? + @NSManaged public var company: String? + @NSManaged public var articleDate: String? + @NSManaged public var image: NSData? + @NSManaged public var isRead: Bool + @NSManaged public var language: String? + @NSManaged public var scrappedDate: NSDate? + @NSManaged public var articleTitle: String? + @NSManaged public var articleUri: String? + @NSManaged public var category: String? +} diff --git a/tree/tree/Model/DataManager/ScrapManager.swift b/tree/tree/Model/DataManager/ScrapManager.swift new file mode 100644 index 0000000..ef60623 --- /dev/null +++ b/tree/tree/Model/DataManager/ScrapManager.swift @@ -0,0 +1,173 @@ +// +// ScrapManager.swift +// tree +// +// Created by Hyeontae on 03/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import Foundation +import CoreData +import UIKit + +final class ScrapManager { + static var managedContext: NSManagedObjectContext = { + guard let appDelegate = + UIApplication.shared.delegate as? AppDelegate else { + return NSManagedObjectContext() + } + let managedContext: NSManagedObjectContext = + appDelegate.persistentContainer.viewContext + + return managedContext + }() + + static func scrapArticle( + article: Article, + category: ArticleCategory, + imageData: Data? + ) -> Void { + guard let entity = + NSEntityDescription.entity(forEntityName: "ScrappedArticle", in: managedContext) else { + return + } + let newArticle = ScrappedArticle(entity: entity, insertInto: managedContext) + newArticle.setValues( + newArticle, + articleData: article, + categoryEnum: category, + imageData: imageData + ) + do { + try managedContext.save() + } catch { + print(error.localizedDescription) + } + } + + static func fetchArticles() -> [ScrappedArticle] { + let request: NSFetchRequest = ScrappedArticle.fetchRequest() + let sortDescriptor: NSSortDescriptor = + NSSortDescriptor( + key: #keyPath(ScrappedArticle.scrappedDate), + ascending: false + ) + request.sortDescriptors = [sortDescriptor] + var result: [ScrappedArticle] = [] + result = fetchRequest(request) + return result + } + + static func fetchArticles(_ category: ArticleCategory) -> [ScrappedArticle] { + var request: NSFetchRequest = ScrappedArticle.fetchRequest() + var result: [ScrappedArticle] = [] + request.predicate = + NSPredicate( + format: "%K == %@", + #keyPath(ScrappedArticle.category), + category.toString() + ) + result = fetchRequest(request) + return result + } + + static func countArticle(_ isRead: Bool?) -> Int { + if let isRead = isRead { + return countArticleFetch(NSPredicate(format: "isRead == %@", NSNumber(value: isRead))) + } + return countArticleFetch(nil) + } + + static func countArticle(category: ArticleCategory, _ isRead: Bool?) -> Int { + var predicate: NSPredicate + if let isRead = isRead { + predicate = NSPredicate( + format: "isRead == %@ AND %K == %@", + NSNumber(value: isRead), + #keyPath(ScrappedArticle.category), + category.toString() + ) + } else { + predicate = NSPredicate( + format: "%K == %@", + #keyPath(ScrappedArticle.category), + category.toString() + ) + } + return countArticleFetch(predicate) + } + + static func countArticleFetch(_ predicate: NSPredicate?) -> Int{ + let request: NSFetchRequest = NSFetchRequest(entityName: "ScrappedArticle") + request.resultType = .countResultType + if let predicate = predicate { + request.predicate = predicate + } + do { + let result = try managedContext.fetch(request) + guard let resultCount = result.first?.intValue else { + return 0 + } + return resultCount + } catch let error as NSError { + print("Could not fetch. \(error), \(error.userInfo)") + } + return 0 + } + + static func removeAllScrappedArticle() { + let request: NSFetchRequest = ScrappedArticle.fetchRequest() + var results: [ScrappedArticle] = [] + do { + results = try managedContext.fetch(request) + for item in results { + managedContext.delete(item) + } + try managedContext.save() + } catch let error as NSError { + print("Could not fetch. \(error), \(error.userInfo)") + } + print("all data is Removed") + } + + static func fetchRequest(_ request: NSFetchRequest) -> [ScrappedArticle] { + var result: [ScrappedArticle] = [] + do { + result = try managedContext.fetch(request) + } catch let error as NSError { + print("Could not fetch. \(error), \(error.userInfo)") + } + return result + } +} + +private extension NSManagedObject { + func setValue(_ value: Any?, forKey property: ScrappedArticleProperty) { + self.setValue(value, forKeyPath: "\(property)") + } + + func setCategory(_ category: ArticleCategory) { + self.setValue(category.toString(), forKey: .category) + } + + func setValues( + _ newArticle: ScrappedArticle, + articleData: Article, + categoryEnum: ArticleCategory, + imageData: Data? + ) { + if let imageData: Data = imageData { + newArticle.setValue(imageData, forKey: .image) + } + newArticle.setCategory(categoryEnum) + newArticle.setValue(articleData.lang, forKey: .language) + newArticle.setValue(articleData.author?[0].name ?? "", forKey: .articleAuthor) + newArticle.setValue(articleData.date, forKey: .articleDate) + newArticle.setValue(articleData.title, forKey: .articleTitle) + newArticle.setValue(NSDate(), forKey: .scrappedDate) + newArticle.setValue(false, forKey: .isRead) + newArticle.setValue(articleData.uri, forKey: .articleUri) + newArticle.setValue(articleData.source.title, forKey: .company) + newArticle.setValue(articleData.body, forKey: .articleDescription) + } +} diff --git a/tree/tree/Model/DataManager/ScrapService.swift b/tree/tree/Model/DataManager/ScrapService.swift new file mode 100644 index 0000000..e1784c5 --- /dev/null +++ b/tree/tree/Model/DataManager/ScrapService.swift @@ -0,0 +1,109 @@ +// +// ScrapService.swift +// tree +// +// Created by Hyeontae on 03/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import Foundation +import UIKit + +enum ScrappedArticleProperty { + case articleAuthor + case articleDescription + case company + case articleDate + case image + case isRead + case language + case scrappedDate + case articleTitle + case articleUri + case category + + func toString() -> String { + return "\(self)" + } +} + +public enum ArticleCategory: CaseIterable{ + case all + case arts + case business + case computers + case games + case health + case home + case recreation + case reference + case regional + case science + case shopping + case society + case sports + + func toString() -> String { + return "\(self)" + } + + func capitalFirstCharactor() -> String { + let baseCategory: NSString = NSString(string: "\(self)") + let firstCharactor = baseCategory.character(at: 0) + guard let unicode = UnicodeScalar(firstCharactor - 32) as? UnicodeScalar else { + return "" + } + let result = baseCategory.replacingCharacters( + in: NSRange(location: 0, length: 1), + with: String(Character(unicode)) + ) + return result + } + + var gradientColors: [CGColor] { + switch self { + case .all: + return [UIColor(hexString: "#f6416c").cgColor, + UIColor(hexString: "#fff6b7").cgColor] + case .arts: + return [UIColor(hexString: "#7b4397").cgColor, + UIColor(hexString: "#dc2430").cgColor] + case .business: + return [UIColor(hexString: "#667eea").cgColor, + UIColor(hexString: "#764ba2").cgColor] + case .computers: + return [UIColor(hexString: "#a3bded").cgColor, + UIColor(hexString: "#6991c7").cgColor] + case .games: + return [UIColor(hexString: "#13547a").cgColor, + UIColor(hexString: "#80d0c7").cgColor] + case .health: + return [UIColor(hexString: "#93a5cf").cgColor, + UIColor(hexString: "#e4efe9").cgColor] + case .home: + return [UIColor(hexString: "#ff9a9e").cgColor, + UIColor(hexString: "#fad0c4").cgColor] + case .recreation: + return [UIColor(hexString: "#868f96").cgColor, + UIColor(hexString: "#596164").cgColor] + case .reference: + return [UIColor(hexString: "#c79081").cgColor, + UIColor(hexString: "#dfa579").cgColor] + case .regional: + return [UIColor(hexString: "#29323c").cgColor, + UIColor(hexString: "#485563").cgColor] + case .science: + return [UIColor(hexString: "#1e3c72").cgColor, + UIColor(hexString: "#2a5298").cgColor] + case .shopping: + return [UIColor(hexString: "#B7F8DB").cgColor, + UIColor(hexString: "#50A7C2").cgColor] + case .society: + return [UIColor(hexString: "#cc2b5e").cgColor, + UIColor(hexString: "#753a88").cgColor] + case .sports: + return [UIColor(hexString: "#42275a").cgColor, + UIColor(hexString: "#734b6d").cgColor] + } + } +} diff --git a/tree/tree/Model/Graph.swift b/tree/tree/Model/Graph.swift new file mode 100644 index 0000000..68bd2a1 --- /dev/null +++ b/tree/tree/Model/Graph.swift @@ -0,0 +1,25 @@ +// +// Graph.swift +// tree +// +// Created by ParkSungJoon on 06/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import Foundation + +struct Graph: Codable { + let startDate, endDate, timeUnit: String + let results: [KeywordResult] +} + +struct KeywordResult: Codable { + let title: String + let keywords: [String] + let data: [Datum] +} + +struct Datum: Codable { + let period: String + let ratio: Double +} diff --git a/tree/tree/Model/TrendDays.swift b/tree/tree/Model/TrendDays.swift new file mode 100644 index 0000000..9daeeaf --- /dev/null +++ b/tree/tree/Model/TrendDays.swift @@ -0,0 +1,71 @@ +// +// TrandDays.swift +// tree +// +// Created by ParkSungJoon on 29/01/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import Foundation + +struct TrendDays: Codable { + let trend: Default + private enum CodingKeys: String, CodingKey { + case trend = "default" + } +} + +struct Default: Codable { + let searchesByDays: [TrendingSearchesDay] + let endDateForNextRequest: String + let rssFeedPageURL: String + private enum CodingKeys: String, CodingKey { + case endDateForNextRequest + case rssFeedPageURL = "rssFeedPageUrl" + case searchesByDays = "trendingSearchesDays" + } +} + +struct TrendingSearchesDay: Codable { + let date, formattedDate: String + let keywordList: [TrendingSearch] + private enum CodingKeys: String, CodingKey { + case date, formattedDate + case keywordList = "trendingSearches" + } +} + +struct TrendingSearch: Codable { + let title: Title + let formattedTraffic: String + let relatedQueries: [Title] + let image: Image + let articles: [KeywordArticles] + let shareURL: String + private enum CodingKeys: String, CodingKey { + case title, formattedTraffic, relatedQueries, image, articles + case shareURL = "shareUrl" + } +} + +struct KeywordArticles: Codable { + let title, timeAgo, source: String + let image: Image? + let url: String + let snippet: String +} + +struct Image: Codable { + let newsURL: String + let source: String + let imageURL: String + private enum CodingKeys: String, CodingKey { + case newsURL = "newsUrl" + case source + case imageURL = "imageUrl" + } +} + +struct Title: Codable { + let query, exploreLink: String +} diff --git a/tree/tree/Network Layer/API/EventRegistryAPI.swift b/tree/tree/Network Layer/API/EventRegistryAPI.swift index 65b16f9..99d2db8 100644 --- a/tree/tree/Network Layer/API/EventRegistryAPI.swift +++ b/tree/tree/Network Layer/API/EventRegistryAPI.swift @@ -11,6 +11,7 @@ import Foundation private enum DefaultParameter { case articleBodyLen case includeArticleImage + case includeArticleCategories case articlesCount case resultType case action @@ -21,6 +22,7 @@ extension DefaultParameter { switch self { case .articleBodyLen: return -1 case .includeArticleImage: return true + case .includeArticleCategories: return true case .articlesCount: return 20 case .resultType: return "articles" case .action: return "getArticles" @@ -39,12 +41,13 @@ enum EventRegistryAPI { } extension EventRegistryAPI: APIService { + var baseURL: URL { guard let url = URL(string: "http://eventregistry.org") else { fatalError("Invalid URL") } return url } - var path: String { + var path: String? { switch self { case .getArticles: return "/api/v1/article" @@ -77,6 +80,7 @@ extension EventRegistryAPI: APIService { "resultType": DefaultParameter.resultType.value, "articlesCount": DefaultParameter.articlesCount.value, "includeArticleImage": DefaultParameter.includeArticleImage.value, + "includeArticleCategories": DefaultParameter.includeArticleCategories.value, "articleBodyLen": DefaultParameter.articleBodyLen.value, "apiKey": APIConstant.eventRegistryKey ] @@ -93,4 +97,8 @@ extension EventRegistryAPI: APIService { ) } } + + var header: HTTPHeader? { + return nil + } } diff --git a/tree/tree/Network Layer/API/GoogleTrendAPI.swift b/tree/tree/Network Layer/API/GoogleTrendAPI.swift new file mode 100644 index 0000000..881d546 --- /dev/null +++ b/tree/tree/Network Layer/API/GoogleTrendAPI.swift @@ -0,0 +1,55 @@ +// +// GoogleTrendAPI.swift +// tree +// +// Created by ParkSungJoon on 29/01/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import Foundation + +enum GoogleTrendAPI { + case getDailyTrends(hl: String, geo: String) +} + +extension GoogleTrendAPI: APIService { + + var baseURL: URL { + guard let url = URL(string: "https://trends.google.com/trends/api") else { + fatalError("Invalid URL") + } + return url + } + + var path: String? { + switch self { + case .getDailyTrends: + return "/dailytrends" + } + } + + var method: HTTPMethod { + switch self { + case .getDailyTrends: + return .get + } + } + + var parameters: Parameters { + switch self { + case .getDailyTrends(let hl, let geo): + return ["hl": hl, "geo": geo] + } + } + + var task: HTTPTask { + switch self { + case .getDailyTrends: + return .requestWith(url: parameters, body: nil, encoding: .query) + } + } + + var header: HTTPHeader? { + return nil + } +} diff --git a/tree/tree/Network Layer/API/NaverAPI.swift b/tree/tree/Network Layer/API/NaverAPI.swift new file mode 100644 index 0000000..4a67c6d --- /dev/null +++ b/tree/tree/Network Layer/API/NaverAPI.swift @@ -0,0 +1,71 @@ +// +// NaverAPI.swift +// tree +// +// Created by ParkSungJoon on 07/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import Foundation + +enum NaverAPIMode { + case keywordTrend( + startDate: String, + endDate: String, + timeUnit: String, + keywordGroups: [Parameters] + ) +} + +extension NaverAPIMode: APIService { + + var baseURL: URL { + guard let url = URL(string: "https://openapi.naver.com/v1/datalab/search") else { + fatalError("Invalid URL") + } + return url + } + + var path: String? { + switch self { + case .keywordTrend: + return nil + } + } + + var method: HTTPMethod { + switch self { + case .keywordTrend: + return .post + } + } + + var parameters: Parameters { + switch self { + case .keywordTrend(let startDate, let endDate, let timeUnit, let keywordGroups): + return [ + "startDate": startDate, + "endDate": endDate, + "timeUnit": timeUnit, + "keywordGroups": keywordGroups + ] + } + } + + var task: HTTPTask { + switch self { + case .keywordTrend: + return .requestWith(url: nil, body: parameters, encoding: .body) + } + } + + var header: HTTPHeader? { + switch self { + case .keywordTrend: + return [ + .naverClientID: APIConstant.naverClientID, + .naverClientSecret: APIConstant.naverClientSecret + ] + } + } +} diff --git a/tree/tree/Network Layer/Service/APICenter.swift b/tree/tree/Network Layer/Service/APICenter.swift index 539ddf0..ee71a8b 100644 --- a/tree/tree/Network Layer/Service/APICenter.swift +++ b/tree/tree/Network Layer/Service/APICenter.swift @@ -8,52 +8,95 @@ import Foundation +typealias HTTPHeader = [HTTPHeaderField: String] + class APICenter { func request( _ service: Service, completion: @escaping ( _ data: Data?, - _ response: URLResponse?, _ error: NetworkError? ) -> Void) { do { let urlRequest = try makeURLRequest(from: service) - let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in + let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, _) in guard let data = data else { - return completion(nil, response, NetworkError.noData) + return completion(nil, NetworkError.noData) } guard let response = response as? HTTPURLResponse else { - return completion(data, nil, NetworkError.noResponse) + return completion(data, NetworkError.noResponse) } let statusCode = response.statusCode switch NetworkResponse().result(response) { case .success: - completion(data, response, nil) + completion(data, nil) case .failure: - completion(data, response, NetworkError.networkFail) + completion(data, NetworkError.networkFail) case .clientError: - completion(data, response, NetworkError.clientError(statusCode: statusCode)) + completion(data, NetworkError.clientError(statusCode: statusCode)) case .serverError: - completion(data, response, NetworkError.serverError(statusCode: statusCode)) + completion(data, NetworkError.serverError(statusCode: statusCode)) } } task.resume() } catch { - completion(nil, nil, NetworkError.makeURLRequestFail) + completion(nil, NetworkError.makeURLRequestFail) + } + } + + func requestDownload( + _ service: Service, + completion: @escaping ( + _ pureJSON: String?, + _ error: NetworkError? + ) -> Void) { + do { + let urlRequest = try makeURLRequest(from: service) + let task = URLSession.shared.downloadTask(with: urlRequest) { (localURL, response, _) in + guard let localURL = localURL else { + return completion(nil, NetworkError.invalidURL) + } + do { + let rawJSON = try String(contentsOf: localURL) + guard let pureJSON = rawJSON.components(separatedBy: "\n").last else { return } + guard let response = response as? HTTPURLResponse else { + return completion(pureJSON, NetworkError.noResponse) + } + let statusCode = response.statusCode + switch NetworkResponse().result(response) { + case .success: + completion(pureJSON, nil) + case .failure: + completion(pureJSON, NetworkError.networkFail) + case .clientError: + completion(pureJSON, NetworkError.clientError(statusCode: statusCode)) + case .serverError: + completion(pureJSON, NetworkError.serverError(statusCode: statusCode)) + } + } catch { + completion(nil, NetworkError.decodingFail) + } + } + task.resume() + } catch { + completion(nil, NetworkError.makeURLRequestFail) } } private func makeURLRequest(from service: Service) throws -> URLRequest { - let fullUrl = service.baseURL.appendingPathComponent(service.path) + var fullUrl = service.baseURL + if let path = service.path { + fullUrl = fullUrl.appendingPathComponent(path) + } var urlRequest = URLRequest(url: fullUrl) urlRequest.httpMethod = service.method.rawValue switch service.task { case .request: - setHeaderField(&urlRequest) + setHeaderField(&urlRequest, header: service.header) case .requestWith(let query, let body, let encoder): do { try encoder.encode(request: &urlRequest, query: query, body: body) - setHeaderField(&urlRequest) + setHeaderField(&urlRequest, header: service.header) } catch { throw error } @@ -61,7 +104,7 @@ class APICenter { return urlRequest } - private func setHeaderField(_ urlRequest: inout URLRequest) { + private func setHeaderField(_ urlRequest: inout URLRequest, header: HTTPHeader?) { urlRequest.setValue( ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.acceptType.rawValue @@ -70,5 +113,9 @@ class APICenter { ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue ) + guard let header = header else { return } + for (key, value) in header { + urlRequest.setValue(value, forHTTPHeaderField: key.rawValue) + } } } diff --git a/tree/tree/Network Layer/Service/APIManager.swift b/tree/tree/Network Layer/Service/APIManager.swift index ef61a9a..e8e6f12 100644 --- a/tree/tree/Network Layer/Service/APIManager.swift +++ b/tree/tree/Network Layer/Service/APIManager.swift @@ -9,7 +9,7 @@ import Foundation final class APIManager { - static func getArticles( + static func fetchArticles( keyword: String, keywordLoc: String, lang: String, @@ -22,7 +22,7 @@ final class APIManager { keywordLoc: keywordLoc, lang: lang, articlesSortBy: articlesSortBy, - articlesPage: articlesPage)) { (data, response, error) in + articlesPage: articlesPage)) { (data, error) in guard error == nil else { return completion(Result.failure(error!)) } @@ -35,4 +35,50 @@ final class APIManager { } } } + + static func fetchDailyTrends( + hl: String, + geo: String, + completion: @escaping (Result + ) -> Void) { + APICenter().requestDownload(.getDailyTrends(hl: hl, geo: geo)) { (prettyJSON, error) in + guard error == nil else { + return completion(Result.failure(error!)) + } + guard let prettyJSON = prettyJSON else { return } + guard let jsonData = prettyJSON.data(using: .utf8) else { return } + do { + let decodeJSON = try JSONDecoder().decode(TrendDays.self, from: jsonData) + completion(Result.success(decodeJSON)) + } catch { + completion(Result.failure(NetworkError.decodingFail)) + } + } + } + + static func requestGraphData( + startDate: String, + endDate: String, + timeUnit: String, + keywordGroups: [[String: Any]], + completion: @escaping (Result + ) -> Void) { + APICenter().request(.keywordTrend( + startDate: startDate, + endDate: endDate, + timeUnit: timeUnit, + keywordGroups: keywordGroups + )) { (data, error) in + guard error == nil else { + return completion(Result.failure(error!)) + } + guard let responseData = data else { return } + do { + let decodeJSON = try JSONDecoder().decode(Graph.self, from: responseData) + completion(Result.success(decodeJSON)) + } catch { + completion(Result.failure(NetworkError.decodingFail)) + } + } + } } diff --git a/tree/tree/Network Layer/Service/APIService.swift b/tree/tree/Network Layer/Service/APIService.swift index 0b04753..07f1087 100644 --- a/tree/tree/Network Layer/Service/APIService.swift +++ b/tree/tree/Network Layer/Service/APIService.swift @@ -10,10 +10,11 @@ import Foundation protocol APIService { var baseURL: URL { get } - var path: String { get } + var path: String? { get } var method: HTTPMethod { get } var parameters: Parameters { get } var task: HTTPTask { get } + var header: HTTPHeader? { get } } enum HTTPMethod: String { @@ -33,6 +34,9 @@ enum HTTPHeaderField: String { case contentType = "Content-Type" case acceptType = "Accept" case acceptEncoding = "Accept-Encoding" + case contentDisposition = "Content-Disposition" + case naverClientID = "X-Naver-Client-Id" + case naverClientSecret = "X-Naver-Client-Secret" } enum ContentType: String { diff --git a/tree/tree/Protocol/HTMLDecodable.swift b/tree/tree/Protocol/HTMLDecodable.swift new file mode 100644 index 0000000..adccb4e --- /dev/null +++ b/tree/tree/Protocol/HTMLDecodable.swift @@ -0,0 +1,34 @@ +// +// HTMLDecodable.swift +// tree +// +// Created by ParkSungJoon on 10/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import Foundation + +protocol HTMLDecodable { + func decode(_ html: String?) -> String? +} + +extension HTMLDecodable { + func decode(_ html: String?) -> String? { + guard let html = html else { return nil } + guard let data = html.data(using: .utf8) else { return nil } + let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [ + NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html, + NSAttributedString.DocumentReadingOptionKey.characterEncoding: String.Encoding.utf8.rawValue + ] + do { + let attributedString = try NSAttributedString( + data: data, + options: options, + documentAttributes: nil + ) + return attributedString.string + } catch { + return nil + } + } +} diff --git a/tree/tree/TreeData.xcdatamodeld/TreeData.xcdatamodel/contents b/tree/tree/TreeData.xcdatamodeld/TreeData.xcdatamodel/contents new file mode 100644 index 0000000..f449afa --- /dev/null +++ b/tree/tree/TreeData.xcdatamodeld/TreeData.xcdatamodel/contents @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tree/tree/View/Helper/LoadingView.swift b/tree/tree/View/Helper/LoadingView.swift new file mode 100644 index 0000000..ff061ac --- /dev/null +++ b/tree/tree/View/Helper/LoadingView.swift @@ -0,0 +1,61 @@ +// +// LoadingView.swift +// tree +// +// Created by hyeri kim on 01/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +class LoadingView: UIView { + + @IBOutlet weak var loadingView: UIView! + @IBOutlet var dots: [UIView]! + + private let xibName = "LoadingView" + + override init(frame: CGRect) { + super.init(frame: frame) + initXIB() + setRadius() + startAnimation() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + initXIB() + setRadius() + startAnimation() + } + + private func initXIB() { + guard let view = Bundle.main.loadNibNamed( + xibName, + owner: self, + options: nil)?.first as? UIView + else { return } + view.frame = self.bounds + self.addSubview(view) + } + + private func setRadius() { + let radius: CGFloat = 5 + for dot in dots { + dot.layer.cornerRadius = radius + } + } + + private func startAnimation() { + for index in 0.. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tree/tree/View/Live/Keyword/Detail/GraphView.swift b/tree/tree/View/Live/Keyword/Detail/GraphView.swift new file mode 100644 index 0000000..615fd5c --- /dev/null +++ b/tree/tree/View/Live/Keyword/Detail/GraphView.swift @@ -0,0 +1,247 @@ +// +// GraphView.swift +// tree +// +// Created by ParkSungJoon on 06/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +class GraphView: UIView { + + private var dataPoints: [CGPoint]? + var graphData: KeywordResult? { + didSet { + self.setNeedsLayout() + } + } + private let dataLayer: CALayer = CALayer() + private let mainLayer: CALayer = CALayer() + private let scrollView: UIScrollView = UIScrollView() + private let gridLayer: CALayer = CALayer() + private let contentSpace: CGFloat = 60 + private let bottomSpace: CGFloat = 30 + private let leftSpace: CGFloat = 30 + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupView() + } + + private func setupView() { + mainLayer.addSublayer(dataLayer) + scrollView.layer.addSublayer(mainLayer) + self.layer.addSublayer(gridLayer) + self.addSubview(scrollView) + } + + override func layoutSubviews() { + guard let graphData = graphData else { return } + if hasGraphData(to: graphData) { + drawScrollViewFrame(from: graphData) + drawGraphLayerFrame(from: graphData) + dataPoints = graphPoints(from: graphData) + removeAllLayer() + drawHorizontalLines() + drawGraph() + drawBottomLabels() + scrollView.showsHorizontalScrollIndicator = false + } else { + drawEmptyLayer() + } + } + + private func hasGraphData(to graphData: KeywordResult) -> Bool { + if graphData.data.count > 1 { + return true + } + return false + } + + private func drawScrollViewFrame(from graphData: KeywordResult) { + scrollView.frame = CGRect( + x: 0, + y: 0, + width: self.frame.size.width, + height: self.frame.size.height + ) + scrollView.contentSize = CGSize( + width: CGFloat(graphData.data.count) * contentSpace + 100, + height: self.frame.size.height + ) + } + + private func drawGraphLayerFrame(from graphData: KeywordResult) { + mainLayer.frame = CGRect( + x: 0, y: 0, width: CGFloat(graphData.data.count) * contentSpace, + height: self.frame.size.height + ) + dataLayer.frame = CGRect( + x: leftSpace, + y: 0, + width: mainLayer.frame.width, + height: mainLayer.frame.height - bottomSpace + ) + gridLayer.frame = CGRect( + x: 0, + y: 0, + width: self.frame.width, + height: mainLayer.frame.height - bottomSpace + ) + } + + private func graphPoints(from graphData: KeywordResult) -> [CGPoint] { + var result: [CGPoint] = [] + for index in 0.. 0 else { + return + } + let lineLayer = drawLineLayer( + path: path, + lineWidth: 3.0, + lineColor: UIColor.brightBlue.cgColor + ) + dataLayer.addSublayer(lineLayer) + } + + private func createPath() -> UIBezierPath? { + guard + let dataPoints = dataPoints, + dataPoints.count > 0 else { + return nil + } + let path = UIBezierPath() + path.move(to: dataPoints[0]) + + for index in 1.. 0 else { + return + } + for index in 0.. 0.0 && gridValues[index] < 1.0 { + lineLayer.lineDashPattern = [4, 4] + } + gridLayer.addSublayer(lineLayer) + + let textLayer = drawTextLayer(alignmentMode: .right, fontSize: 12) + textLayer.frame = CGRect( + x: 0, + y: height - 7, + width: 20, + height: 16 + ) + let reverseIndexValue = gridValues[gridValues.count - 1 - index] + textLayer.string = "\(Int(reverseIndexValue * 100))" + gridLayer.addSublayer(textLayer) + } + } + + private func removeAllLayer() { + mainLayer.sublayers?.forEach({ + if $0 is CATextLayer { + $0.removeFromSuperlayer() + } + }) + dataLayer.sublayers?.forEach({$0.removeFromSuperlayer()}) + gridLayer.sublayers?.forEach({$0.removeFromSuperlayer()}) + } + + private func drawTextLayer( + alignmentMode: CATextLayerAlignmentMode, + fontSize: CGFloat + ) -> CATextLayer { + let textLayer = CATextLayer() + textLayer.foregroundColor = UIColor.whiteGray.cgColor + textLayer.backgroundColor = UIColor.clear.cgColor + textLayer.contentsScale = UIScreen.main.scale + textLayer.alignmentMode = CATextLayerAlignmentMode(rawValue: alignmentMode.rawValue) + textLayer.fontSize = fontSize + return textLayer + } + + private func drawLineLayer( + path: UIBezierPath, + lineWidth: CGFloat, + lineColor: CGColor + ) -> CAShapeLayer { + let lineLayer = CAShapeLayer() + lineLayer.path = path.cgPath + lineLayer.fillColor = UIColor.clear.cgColor + lineLayer.strokeColor = lineColor + lineLayer.lineWidth = lineWidth + return lineLayer + } + + private func drawEmptyLayer() { + let emptyLayer = CALayer() + emptyLayer.frame = CGRect( + x: 0, + y: 0, + width: self.frame.width, + height: self.frame.height + ) + self.layer.addSublayer(emptyLayer) + let textLayer = drawTextLayer(alignmentMode: .center, fontSize: 40) + textLayer.frame = emptyLayer.frame + textLayer.string = "\n😭" + emptyLayer.addSublayer(textLayer) + } +} diff --git a/tree/tree/View/Live/Keyword/Detail/KeywordDetailArticleCell.swift b/tree/tree/View/Live/Keyword/Detail/KeywordDetailArticleCell.swift new file mode 100644 index 0000000..3c8c5b5 --- /dev/null +++ b/tree/tree/View/Live/Keyword/Detail/KeywordDetailArticleCell.swift @@ -0,0 +1,51 @@ +// +// KeywordDetailArticleCell.swift +// tree +// +// Created by ParkSungJoon on 10/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +class KeywordDetailArticleCell: UITableViewCell, HTMLDecodable { + + @IBOutlet weak var backgroundContainerView: UIView! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var pressLabel: UILabel! + @IBOutlet weak var timeAgoLabel: UILabel! + + private var shadowView: UIView { + let shadowView = UIView( + frame: CGRect( + x: innerMargin, + y: innerMargin, + width: bounds.width - (2 * innerMargin), + height: bounds.height - (2 * innerMargin)) + ) + insertSubview(shadowView, at: 0) + return shadowView + } + private let innerMargin: CGFloat = 20.0 + + override func awakeFromNib() { + super.awakeFromNib() + roundCorners(layer: backgroundContainerView.layer, radius: 14) + self.applyShadow( + shadowView: shadowView, + width: CGFloat(0.0), + height: CGFloat(0.0) + ) + } + + func configure(_ article: KeywordArticles) { + guard + let title = decode(article.title), + let press = decode(article.source) else { + return + } + titleLabel.text = title + pressLabel.text = press + timeAgoLabel.text = article.timeAgo + } +} diff --git a/tree/tree/View/Live/Keyword/Detail/KeywordDetailArticleCell.xib b/tree/tree/View/Live/Keyword/Detail/KeywordDetailArticleCell.xib new file mode 100644 index 0000000..8f8cb27 --- /dev/null +++ b/tree/tree/View/Live/Keyword/Detail/KeywordDetailArticleCell.xib @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tree/tree/View/Live/Keyword/Detail/KeywordDetailGraphCell.swift b/tree/tree/View/Live/Keyword/Detail/KeywordDetailGraphCell.swift new file mode 100644 index 0000000..039a9af --- /dev/null +++ b/tree/tree/View/Live/Keyword/Detail/KeywordDetailGraphCell.swift @@ -0,0 +1,44 @@ +// +// KeywordDetailGraphCell.swift +// tree +// +// Created by ParkSungJoon on 06/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +class KeywordDetailGraphCell: UITableViewCell { + + @IBOutlet weak var backgroundContainerView: UIView! + @IBOutlet weak var graphView: GraphView! + + private var shadowView: UIView { + let shadowView = UIView( + frame: CGRect( + x: innerMargin, + y: innerMargin, + width: bounds.width - (2 * innerMargin), + height: bounds.height - (2 * innerMargin)) + ) + insertSubview(shadowView, at: 0) + return shadowView + } + private let innerMargin: CGFloat = 20.0 + + var graphData: KeywordResult? { + didSet { + graphView.graphData = graphData + } + } + + override func awakeFromNib() { + super.awakeFromNib() + roundCorners(layer: backgroundContainerView.layer, radius: 14) + self.applyShadow( + shadowView: shadowView, + width: CGFloat(0.0), + height: CGFloat(0.0) + ) + } +} diff --git a/tree/tree/View/Live/Keyword/Detail/KeywordDetailGraphCell.xib b/tree/tree/View/Live/Keyword/Detail/KeywordDetailGraphCell.xib new file mode 100644 index 0000000..f5d9e09 --- /dev/null +++ b/tree/tree/View/Live/Keyword/Detail/KeywordDetailGraphCell.xib @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tree/tree/View/Live/Keyword/TrendHeaderCell.swift b/tree/tree/View/Live/Keyword/TrendHeaderCell.swift new file mode 100644 index 0000000..c8958f6 --- /dev/null +++ b/tree/tree/View/Live/Keyword/TrendHeaderCell.swift @@ -0,0 +1,153 @@ +// +// TrendHeaderCell.swift +// tree +// +// Created by ParkSungJoon on 02/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +class HeaderCellContent { + var title: String + var country: String + var expanded: Bool + + init(title: String, country: String) { + self.title = title + self.country = country + self.expanded = false + } +} + +class TrendHeaderCell: UITableViewCell { + + @IBOutlet weak var backgroundContainerView: UIView! + @IBOutlet weak var expandingStackView: UIStackView! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var countryLabel: UILabel! + @IBOutlet weak var grayLineView: UIView! + @IBOutlet var countryButtons: [UIButton]! + @IBOutlet var switchableConstraints: [NSLayoutConstraint]! + + private var zeroHeightConstraint: NSLayoutConstraint? + private weak var shadowView: UIView? + private let innerMargin: CGFloat = 20.0 + + override func awakeFromNib() { + super.awakeFromNib() + zeroHeightConstraint = expandingStackView.heightAnchor.constraint(equalToConstant: 0) + setButtonTagAndAction(at: countryButtons) + makeRoundView(for: backgroundContainerView) + countryButtons.forEach { (button) in + makeCountryButtonUI( + for: button, + radius: 20, + borderWidth: 1.0, + borderColor: #colorLiteral(red: 0.5921568627, green: 0.5921568627, blue: 0.5921568627, alpha: 1), + textColor: #colorLiteral(red: 0.5921568627, green: 0.5921568627, blue: 0.5921568627, alpha: 1) + ) + } + } + + override func layoutSubviews() { + shadowView?.removeFromSuperview() + configureShadow() + } + + func configure(by content: HeaderCellContent) { + titleLabel.text = content.title + countryLabel.text = content.country + hideExpandedViewIf(content.expanded) + } + + private func hideExpandedViewIf(_ expanded: Bool) { + expandingStackView.isHidden = !expanded + grayLineView.isHidden = !expanded + zeroHeightConstraint?.isActive = !expanded + expandingStackView.spacing = expanded ? 20 : 0 + for constraint in switchableConstraints { + constraint.isActive = expanded + } + } + + private func configureShadow() { + let shadowView = UIView( + frame: CGRect( + x: innerMargin, + y: innerMargin, + width: bounds.width - (2 * innerMargin), + height: bounds.height - (2 * innerMargin)) + ) + insertSubview(shadowView, at: 0) + self.shadowView = shadowView + self.applyShadow( + shadowView: shadowView, + width: CGFloat(0.0), + height: CGFloat(0.0) + ) + } + + private func makeRoundView(for view: UIView) { + view.layer.cornerRadius = 14 + } + + private func makeCountryButtonUI( + for button: UIButton, + radius: CGFloat, + borderWidth: CGFloat, + borderColor: CGColor, + textColor: UIColor + ) { + button.layer.cornerRadius = radius + button.layer.borderWidth = borderWidth + button.layer.borderColor = borderColor + button.setTitleColor(textColor, for: .normal) + } + + private func setButtonTagAndAction(at buttons: [UIButton]) { + for index in 0.. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tree/tree/View/Live/Keyword/TrendListCell.swift b/tree/tree/View/Live/Keyword/TrendListCell.swift new file mode 100644 index 0000000..3a057f6 --- /dev/null +++ b/tree/tree/View/Live/Keyword/TrendListCell.swift @@ -0,0 +1,53 @@ +// +// TrandTableViewCell.swift +// tree +// +// Created by ParkSungJoon on 30/01/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +class TrendListCell: UITableViewCell, HTMLDecodable { + + @IBOutlet weak var backgroundContainerView: UIView! + @IBOutlet weak var hitsLabel: UILabel! + @IBOutlet weak var rankLabel: UILabel! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var subscriptLabel: UILabel! + + private weak var shadowView: UIView? + private let innerMargin: CGFloat = 20.0 + + override func awakeFromNib() { + super.awakeFromNib() + backgroundContainerView.layer.cornerRadius = 14 + configureShadow() + } + + func configure(by content: Default, with section: Int, _ row: Int) { + titleLabel.text = content.searchesByDays[section].keywordList[row].title.query + rankLabel.text = "\(row + 1)" + guard + let article = content.searchesByDays[section] + .keywordList[row] + .articles.first else { return } + subscriptLabel.text = decode(article.title) + hitsLabel.text = content.searchesByDays[section].keywordList[row].formattedTraffic + } + + private func configureShadow() { + let shadowView = UIView(frame: CGRect(x: innerMargin, + y: innerMargin, + width: bounds.width - (2 * innerMargin), + height: bounds.height - (2 * innerMargin) + )) + insertSubview(shadowView, at: 0) + self.shadowView = shadowView + applyShadow( + shadowView: shadowView, + width: CGFloat(0.0), + height: CGFloat(0.0) + ) + } +} diff --git a/tree/tree/View/Live/Keyword/TrendListCell.xib b/tree/tree/View/Live/Keyword/TrendListCell.xib new file mode 100644 index 0000000..6f1b0b8 --- /dev/null +++ b/tree/tree/View/Live/Keyword/TrendListCell.xib @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tree/tree/View/Live/Keyword/TrendListHeaderCell.swift b/tree/tree/View/Live/Keyword/TrendListHeaderCell.swift new file mode 100644 index 0000000..335cc36 --- /dev/null +++ b/tree/tree/View/Live/Keyword/TrendListHeaderCell.swift @@ -0,0 +1,25 @@ +// +// TrandDateHeaderCell.swift +// tree +// +// Created by ParkSungJoon on 31/01/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +class TrendListHeaderCell: UITableViewCell { + + @IBOutlet weak var headerLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } +} diff --git a/tree/tree/View/Live/Keyword/TrendListHeaderCell.xib b/tree/tree/View/Live/Keyword/TrendListHeaderCell.xib new file mode 100644 index 0000000..5da4372 --- /dev/null +++ b/tree/tree/View/Live/Keyword/TrendListHeaderCell.xib @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tree/tree/View/Live/Live.storyboard b/tree/tree/View/Live/Live.storyboard new file mode 100644 index 0000000..9700673 --- /dev/null +++ b/tree/tree/View/Live/Live.storyboard @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tree/tree/View/Live/LiveFeedTableViewCell.swift b/tree/tree/View/Live/LiveFeedTableViewCell.swift new file mode 100644 index 0000000..88040db --- /dev/null +++ b/tree/tree/View/Live/LiveFeedTableViewCell.swift @@ -0,0 +1,27 @@ +// +// LiveFeedTableViewCell.swift +// tree +// +// Created by hyeri kim on 09/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +class LiveFeedTableViewCell: UITableViewCell { + + @IBOutlet weak var dateLabel: UILabel! + @IBOutlet weak var eventLabel: UILabel! + @IBOutlet weak var locationLabel: UILabel! + @IBOutlet weak var articleCountLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + } + +} diff --git a/tree/tree/View/Live/LiveFeedTableViewCell.xib b/tree/tree/View/Live/LiveFeedTableViewCell.xib new file mode 100644 index 0000000..618c76b --- /dev/null +++ b/tree/tree/View/Live/LiveFeedTableViewCell.xib @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tree/tree/View/Live/TableViewCell+Animator.swift b/tree/tree/View/Live/TableViewCell+Animator.swift new file mode 100644 index 0000000..1cc0f7f --- /dev/null +++ b/tree/tree/View/Live/TableViewCell+Animator.swift @@ -0,0 +1,63 @@ +// +// TableViewCell+Animator.swift +// tree +// +// Created by ParkSungJoon on 02/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +/// TableView willDisplayμ—μ„œ 인자λ₯Ό λ„˜κ²¨ λ°›μ•„ μ²˜λ¦¬ν•˜κΈ° μœ„ν•œ return typealias +typealias Animation = (UITableViewCell, IndexPath, UITableView) -> Void + +/// TableViewCell에 λŒ€ν•œ λ‹€μ–‘ν•œ μ• λ‹ˆλ©”μ΄μ…˜ 효과 λ©”μ†Œλ“œλ₯Ό μ •μ˜ν•  수 μžˆλŠ” Factory +enum AnimationFactory { + + /** + μ•„ν•€λ³€ν˜•μ„ μ΄μš©ν•΄ TableViewCell을 μœ„λ‘œ μ›€μ§μ΄λ©΄μ„œ Fade효과λ₯Ό 쀄 수 μžˆλŠ” λ©”μ†Œλ“œ + - Parameters: + - rowHeight: Cell row의 높이 + - duration: Animation 지속 μ‹œκ°„ + - delayFactor: Animate 효과 지연 μ‹œκ°„ + */ + static func makeMoveUpWithFade( + rowHeight: CGFloat, + duration: TimeInterval, + delayFactor: Double + ) -> Animation { + return { cell, indexPath, _ in + cell.transform = CGAffineTransform(translationX: 0, y: rowHeight / 2) + cell.alpha = 0 + UIView.animate( + withDuration: duration, + delay: delayFactor * Double(indexPath.row), + options: [.curveEaseInOut], + animations: { + cell.transform = CGAffineTransform(translationX: 0, y: 0) + cell.alpha = 1 + }) + } + } +} + +final class Animator { + private var hasAnimatedCells = false + private let animation: Animation + + init(animation: @escaping Animation) { + self.animation = animation + } + + /// ν˜„μž¬ ν‘œμ‹œλ˜λŠ” TableViewCell의 λ§ˆμ§€λ§‰ indexPath Cell을 μΈμ§€ν•˜μ—¬ hasAnimatedCells 값을 true둜 λ³€ν™˜ν•˜λŠ” λ©”μ†Œλ“œ + func animate(to cell: UITableViewCell, at indexPath: IndexPath, in tableView: UITableView) { + guard !hasAnimatedCells else { return } + animation(cell, indexPath, tableView) + guard let lastIndexPath = tableView.indexPathsForVisibleRows?.last else { return } + if lastIndexPath == indexPath { + hasAnimatedCells = true + } else { + hasAnimatedCells = false + } + } +} diff --git a/tree/tree/View/Live/TrendPageView.swift b/tree/tree/View/Live/TrendPageView.swift new file mode 100644 index 0000000..0feaff0 --- /dev/null +++ b/tree/tree/View/Live/TrendPageView.swift @@ -0,0 +1,167 @@ +// +// TrandPageView.swift +// tree +// +// Created by ParkSungJoon on 30/01/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +protocol PushViewControllerDelegate: class { + func pushViewControllerWhenDidSelectRow(with rowData: TrendingSearch) +} + +class TrendPageView: UIView { + + @IBOutlet weak var tableView: UITableView! + + private let headerCellIdentifier = "TrendHeaderCell" + private let listCellIdentifier = "TrendListCell" + private let listHeaderCellIdentifier = "TrendListHeaderCell" + weak var delegate: PushViewControllerDelegate? + var googleTrendData: TrendDays? + var daysKeywordChart: HeaderCellContent? + + override func awakeFromNib() { + super.awakeFromNib() + registerTableView() + setTableView() + } + + private func registerTableView() { + let headerNib = UINib(nibName: headerCellIdentifier, bundle: nil) + tableView.register(headerNib, forCellReuseIdentifier: headerCellIdentifier) + let listNib = UINib(nibName: listCellIdentifier, bundle: nil) + tableView.register(listNib, forCellReuseIdentifier: listCellIdentifier) + let dateHeaderNib = UINib(nibName: listHeaderCellIdentifier, bundle: nil) + tableView.register(dateHeaderNib, forCellReuseIdentifier: listHeaderCellIdentifier) + } + + private func setTableView() { + tableView.delegate = self + tableView.dataSource = self + tableView.separatorInset = UIEdgeInsets( + top: 0, + left: UIScreen.main.bounds.size.width, + bottom: 0, + right: 0 + ) + } +} + +extension TrendPageView: UITableViewDelegate, UITableViewDataSource { + + func numberOfSections(in tableView: UITableView) -> Int { + guard + let sectionCount = googleTrendData?.trend.searchesByDays.count else { + return 1 + } + return sectionCount + 1 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch section { + case 0: + return 1 + default: + guard + let listBySection = googleTrendData?.trend + .searchesByDays[section-1] + .keywordList else { + return 0 + } + return listBySection.count + } + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + switch section { + case 0: + return UIView() + default: + guard + let headerCell = tableView.dequeueReusableCell( + withIdentifier: listHeaderCellIdentifier + ) as? TrendListHeaderCell else { + return UIView() + } + headerCell.backgroundColor = UIColor.white + headerCell.headerLabel.text = googleTrendData?.trend.searchesByDays[section-1] + .formattedDate + return headerCell.contentView + } + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + switch section { + case 0: + return 10 + default: + return 50 + } + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch indexPath.section { + case 0: + guard let cell = tableView.dequeueReusableCell( + withIdentifier: headerCellIdentifier, + for: indexPath + ) as? TrendHeaderCell else { + return UITableViewCell() + } + guard let headerData = daysKeywordChart else { + return UITableViewCell() + } + cell.configure(by: headerData) + return cell + default: + guard + let cell = tableView.dequeueReusableCell( + withIdentifier: listCellIdentifier, + for: indexPath + ) as? TrendListCell else { + return UITableViewCell() + } + guard let keywordData = googleTrendData?.trend else { + return UITableViewCell() + } + let section = indexPath.section - 1 + let row = indexPath.row + cell.configure(by: keywordData, with: section, row) + return cell + } + } + + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + switch indexPath.section { + case 0: break + default: + let animation = AnimationFactory.makeMoveUpWithFade( + rowHeight: cell.frame.height, + duration: 0.3, + delayFactor: 0.05 + ) + let animator = Animator(animation: animation) + animator.animate(to: cell, at: indexPath, in: tableView) + } + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + switch indexPath.section { + case 0: + guard let headerData = daysKeywordChart else { return } + headerData.expanded = !headerData.expanded + tableView.reloadRows(at: [indexPath], with: .automatic) + default: + guard + let keywordRowData = googleTrendData?.trend + .searchesByDays[indexPath.section - 1] + .keywordList[indexPath.row] else { + return + } + delegate?.pushViewControllerWhenDidSelectRow(with: keywordRowData) + } + } +} diff --git a/tree/tree/View/Live/TrendPageView.xib b/tree/tree/View/Live/TrendPageView.xib new file mode 100644 index 0000000..097d0b0 --- /dev/null +++ b/tree/tree/View/Live/TrendPageView.xib @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tree/tree/View/Scrap/Scrap.storyboard b/tree/tree/View/Scrap/Scrap.storyboard new file mode 100644 index 0000000..4c98664 --- /dev/null +++ b/tree/tree/View/Scrap/Scrap.storyboard @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tree/tree/View/Scrap/ScrapFilter.storyboard b/tree/tree/View/Scrap/ScrapFilter.storyboard new file mode 100644 index 0000000..fe4b9fa --- /dev/null +++ b/tree/tree/View/Scrap/ScrapFilter.storyboard @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tree/tree/View/Scrap/ScrapFilterTableViewCell.swift b/tree/tree/View/Scrap/ScrapFilterTableViewCell.swift new file mode 100644 index 0000000..979daa2 --- /dev/null +++ b/tree/tree/View/Scrap/ScrapFilterTableViewCell.swift @@ -0,0 +1,55 @@ +// +// ScrapFilterTableViewCell.swift +// tree +// +// Created by Hyeontae on 06/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +class ScrapFilterTableViewCell: UITableViewCell { + + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var descriptionLabel: UILabel! + @IBOutlet weak var cardBackgroudView: UIView! + @IBOutlet weak var forGradientView: UIView! + + override func awakeFromNib() { + super.awakeFromNib() + roundCorners(layer: cardBackgroudView.layer, radius: 15.0) + } + + func setAllCategory(width: CGFloat) { + setGradientLayer(.all, width: width) + titleLabel.text = "All Category" + descriptionLabel.text = "\(ScrapManager.countArticle(nil)) articles" + } + + func configure(_ category: ArticleCategory, width: CGFloat) { + setGradientLayer(category, width: width) + setTitleLabel(category) + setDescriptionLabel(category) + } + + func setGradientLayer(_ category: ArticleCategory, width: CGFloat) { + cardBackgroudView.backgroundColor = .clear + let gradientLayer = CAGradientLayer() + // gradientLayer.frame = cell.forGradientView.bounds + gradientLayer.frame = CGRect(x: 0, y: 0, width: width, height: 120) + gradientLayer.cornerRadius = 15.0 + gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5) + gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5) + gradientLayer.colors = category.gradientColors + forGradientView.layer.addSublayer(gradientLayer) + } + + func setTitleLabel(_ category: ArticleCategory) { + titleLabel.text = category.capitalFirstCharactor() + } + + func setDescriptionLabel(_ category: ArticleCategory) { + let articleCount = ScrapManager.countArticle(category: category, nil) + descriptionLabel.text = "\(articleCount) articles" + } +} diff --git a/tree/tree/View/Scrap/ScrapFilterTableViewCell.xib b/tree/tree/View/Scrap/ScrapFilterTableViewCell.xib new file mode 100644 index 0000000..cd988c7 --- /dev/null +++ b/tree/tree/View/Scrap/ScrapFilterTableViewCell.xib @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tree/tree/View/Search/ArticleDetail.storyboard b/tree/tree/View/Search/ArticleDetail.storyboard index 23ad410..4ed09a1 100644 --- a/tree/tree/View/Search/ArticleDetail.storyboard +++ b/tree/tree/View/Search/ArticleDetail.storyboard @@ -1,10 +1,11 @@ - + - + + @@ -45,7 +46,7 @@ - diff --git a/tree/tree/View/Search/ArticleFeedTableViewCell.swift b/tree/tree/View/Search/ArticleFeedTableViewCell.swift index 062b5bd..440b60e 100644 --- a/tree/tree/View/Search/ArticleFeedTableViewCell.swift +++ b/tree/tree/View/Search/ArticleFeedTableViewCell.swift @@ -10,24 +10,57 @@ import UIKit class ArticleFeedTableViewCell: UITableViewCell { + @IBOutlet weak var betweenLabel: UILabel! + @IBOutlet weak var shadowView: UIView! + @IBOutlet weak var imageStackView: UIStackView! + @IBOutlet weak var outerStackView: UIStackView! @IBOutlet weak var articleOuterView: UIView! - @IBOutlet weak var articleImageView: UIImageView! + @IBOutlet weak var articleImageView: ArticleImage! @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var companyLabel: UILabel! @IBOutlet weak var dateLabel: UILabel! @IBOutlet weak var authorLabel: UILabel! @IBOutlet weak var descriptionLabel: UILabel! - + override func awakeFromNib() { super.awakeFromNib() - settingArticleOuterView() + roundConersSetup() + settingShadow() } override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) } - private func settingArticleOuterView() { - articleOuterView.layer.cornerRadius = 10 + private func roundConersSetup() { + articleOuterView.roundCorners(layer: articleOuterView.layer, radius: 10) + } + + private func settingShadow() { + shadowView.layer.masksToBounds = false + shadowView.layer.shadowOpacity = 0.15 + shadowView.layer.shadowRadius = 10.0 + shadowView.layer.shadowOffset = CGSize(width: 0, height: 0) + shadowView.layer.shadowColor = UIColor.darkGray.cgColor + } + + func settingData(article: Article) { + self.titleLabel.text = article.title + self.descriptionLabel.text = article.body + self.dateLabel.text = article.date + self.companyLabel.text = article.source.title + betweenLabel.isHidden = true + if article.author?.isEmpty == false { + if let author = article.author?[0].name { + betweenLabel.isHidden = false + self.authorLabel.text = author + } + } + if let articleImage = article.image { + self.imageStackView.isHidden = false + self.articleImageView.loadImageUrl(articleUrl: articleImage) + } else { + self.imageStackView.isHidden = true + } } } diff --git a/tree/tree/View/Search/ArticleFeedTableViewCell.xib b/tree/tree/View/Search/ArticleFeedTableViewCell.xib index 6910655..b1d128f 100644 --- a/tree/tree/View/Search/ArticleFeedTableViewCell.xib +++ b/tree/tree/View/Search/ArticleFeedTableViewCell.xib @@ -1,10 +1,10 @@ - + - + @@ -12,86 +12,115 @@ - - + + - - + + - - + + - - + + - - - - - - - - - - + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + - + - - - - - - + + + + - - - - + + + + @@ -99,15 +128,16 @@ + + + + - + - - - diff --git a/tree/tree/View/Search/DefaultLabelView.xib b/tree/tree/View/Search/DefaultLabelView.xib new file mode 100644 index 0000000..d3a8365 --- /dev/null +++ b/tree/tree/View/Search/DefaultLabelView.xib @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tree/tree/View/Search/DefautLabelView.swift b/tree/tree/View/Search/DefautLabelView.swift new file mode 100644 index 0000000..0aea9d7 --- /dev/null +++ b/tree/tree/View/Search/DefautLabelView.swift @@ -0,0 +1,46 @@ +// +// DefautLabelView.swift +// tree +// +// Created by hyeri kim on 08/02/2019. +// Copyright Β© 2019 gardener. All rights reserved. +// + +import UIKit + +class DefaultLabelView: UIView { + + @IBOutlet weak var outerView: UIView! + @IBOutlet weak var defaultMessage: UILabel! + @IBOutlet weak var defaultImage: UIImageView! + + private let xibName = "DefaultLabelView" + + override init(frame: CGRect) { + super.init(frame: frame) + initXIB() + setRadius() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + initXIB() + setRadius() + } + + private func initXIB() { + guard let view = Bundle.main.loadNibNamed( + xibName, + owner: self, + options: nil)?.first as? UIView + else { return } + view.frame = self.bounds + self.addSubview(view) + } + + private func setRadius() { + let radius: CGFloat = 10 + defaultImage.roundCorners(layer: defaultImage.layer, radius: radius * 5) + outerView.roundCorners(layer: outerView.layer, radius: radius) + } +} diff --git a/tree/tree/View/Search/Search.storyboard b/tree/tree/View/Search/Search.storyboard index cb8b7bc..34e9bba 100644 --- a/tree/tree/View/Search/Search.storyboard +++ b/tree/tree/View/Search/Search.storyboard @@ -1,11 +1,12 @@ - + - + + @@ -17,8 +18,8 @@ - - + + @@ -45,6 +46,7 @@ + @@ -70,12 +72,18 @@ - - + + - + @@ -85,11 +93,11 @@ - + - - + + @@ -104,26 +112,193 @@ - - + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + - - + + - + + +