├── .github └── FUNDING.yml ├── LICENSE ├── NYTimes Screenshots ├── ArticleScreen.png ├── Banner@0.25x.png ├── BookmarksScreen.png ├── HomeScreen.png ├── HomeScreenDark.png ├── MVVM.jpeg ├── ManageBookmarks.png ├── bookmark3d.png └── gifs │ ├── HomeLoading.gif │ ├── bookmark3d.gif │ ├── browseByCategories.gif │ └── manageBookmark.gif ├── NYTimes.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ └── sameernawaz.xcuserdatad │ │ └── UserInterfaceState.xcuserstate ├── xcshareddata │ └── xcschemes │ │ └── NYTimes.xcscheme └── xcuserdata │ ├── sameernawaz.xcuserdatad │ └── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ └── waseemakram.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── NYTimes ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── 100.png │ │ ├── 1024.png │ │ ├── 114.png │ │ ├── 120.png │ │ ├── 128.png │ │ ├── 144.png │ │ ├── 152.png │ │ ├── 16.png │ │ ├── 167.png │ │ ├── 180.png │ │ ├── 20.png │ │ ├── 256.png │ │ ├── 29.png │ │ ├── 32.png │ │ ├── 40.png │ │ ├── 50.png │ │ ├── 512.png │ │ ├── 57.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 64.png │ │ ├── 72.png │ │ ├── 76.png │ │ ├── 80.png │ │ ├── 87.png │ │ └── Contents.json │ ├── Contents.json │ └── categoryBackground.colorset │ │ └── Contents.json ├── Extensions │ └── Reachability + Extensions.swift ├── Globals │ └── Constants.swift ├── Info.plist ├── Model │ ├── Article.swift │ ├── Bookmark.swift │ ├── Category.swift │ └── Coredata model │ │ ├── CDArticle+CoreDataClass.swift │ │ └── CDArticle+CoreDataProperties.swift ├── Persistence │ ├── NYTimes.xcdatamodeld │ │ ├── .xccurrentversion │ │ └── NYTimes.xcdatamodel │ │ │ └── contents │ └── PersistenceManager.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Repository │ └── BookmarkRepository.swift ├── Supporting files │ ├── AppDelegate.swift │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ └── SceneDelegate.swift ├── Utilities │ ├── Common Utils.swift │ ├── HTMLScarperUtility.swift │ └── NetworkReachabilty.swift ├── ViewModel │ ├── ArticleViewModel.swift │ └── BookmarkViewModel.swift └── Views │ ├── BookmarksView.swift │ ├── Categories │ ├── CategoriesView.swift │ └── CategoriesViewModel.swift │ ├── CategorySelector.swift │ ├── NewsFeedView.swift │ ├── RootView.swift │ ├── WebView.swift │ └── WebViewHolder.swift ├── NYTimesTests ├── ArticleRepositoryTest.swift ├── NYTimesTestInfo.plist └── NYTimesTests.swift └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: devwaseem # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: https://www.paypal.me/iamwaseem99 # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 The Code Monks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NYTimes Screenshots/ArticleScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes Screenshots/ArticleScreen.png -------------------------------------------------------------------------------- /NYTimes Screenshots/Banner@0.25x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes Screenshots/Banner@0.25x.png -------------------------------------------------------------------------------- /NYTimes Screenshots/BookmarksScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes Screenshots/BookmarksScreen.png -------------------------------------------------------------------------------- /NYTimes Screenshots/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes Screenshots/HomeScreen.png -------------------------------------------------------------------------------- /NYTimes Screenshots/HomeScreenDark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes Screenshots/HomeScreenDark.png -------------------------------------------------------------------------------- /NYTimes Screenshots/MVVM.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes Screenshots/MVVM.jpeg -------------------------------------------------------------------------------- /NYTimes Screenshots/ManageBookmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes Screenshots/ManageBookmarks.png -------------------------------------------------------------------------------- /NYTimes Screenshots/bookmark3d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes Screenshots/bookmark3d.png -------------------------------------------------------------------------------- /NYTimes Screenshots/gifs/HomeLoading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes Screenshots/gifs/HomeLoading.gif -------------------------------------------------------------------------------- /NYTimes Screenshots/gifs/bookmark3d.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes Screenshots/gifs/bookmark3d.gif -------------------------------------------------------------------------------- /NYTimes Screenshots/gifs/browseByCategories.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes Screenshots/gifs/browseByCategories.gif -------------------------------------------------------------------------------- /NYTimes Screenshots/gifs/manageBookmark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes Screenshots/gifs/manageBookmark.gif -------------------------------------------------------------------------------- /NYTimes.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 732DDA5E27125B650006F018 /* CategoriesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 732DDA5D27125B650006F018 /* CategoriesView.swift */; }; 11 | 732DDA6027125B6D0006F018 /* CategoriesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 732DDA5F27125B6D0006F018 /* CategoriesViewModel.swift */; }; 12 | 75117C2024D4294D0097E28F /* BookmarkRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75117C1F24D4294D0097E28F /* BookmarkRepository.swift */; }; 13 | 75117C2424D43CE90097E28F /* BookmarkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75117C2324D43CE90097E28F /* BookmarkViewModel.swift */; }; 14 | 7524922624D053CE00B1ACA5 /* NewsFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7524922524D053CE00B1ACA5 /* NewsFeedView.swift */; }; 15 | 7533BB3724D9E00C00A4AF49 /* HTMLScarperUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7533BB3624D9E00C00A4AF49 /* HTMLScarperUtility.swift */; }; 16 | 754C9A1D24D1500C00706469 /* Article.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754C9A1C24D1500C00706469 /* Article.swift */; }; 17 | 754FAFE724D0308900ACFE9E /* Category.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754FAFE624D0308900ACFE9E /* Category.swift */; }; 18 | 7552547224D5E2E700E2143D /* CDArticle+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7552547024D5E2E700E2143D /* CDArticle+CoreDataClass.swift */; }; 19 | 7552547324D5E2E700E2143D /* CDArticle+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7552547124D5E2E700E2143D /* CDArticle+CoreDataProperties.swift */; }; 20 | 7557FA7B24D0271400F80607 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7557FA7A24D0271400F80607 /* AppDelegate.swift */; }; 21 | 7557FA7D24D0271400F80607 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7557FA7C24D0271400F80607 /* SceneDelegate.swift */; }; 22 | 7557FA8024D0271400F80607 /* NYTimes.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 7557FA7E24D0271400F80607 /* NYTimes.xcdatamodeld */; }; 23 | 7557FA8224D0271400F80607 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7557FA8124D0271400F80607 /* RootView.swift */; }; 24 | 7557FA8424D0271500F80607 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7557FA8324D0271500F80607 /* Assets.xcassets */; }; 25 | 7557FA8724D0271500F80607 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7557FA8624D0271500F80607 /* Preview Assets.xcassets */; }; 26 | 7557FA8A24D0271500F80607 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7557FA8824D0271500F80607 /* LaunchScreen.storyboard */; }; 27 | 7557FAA024D0277200F80607 /* CategorySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7557FA9F24D0277200F80607 /* CategorySelector.swift */; }; 28 | 757C5E8724D1B5680024C2AE /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 757C5E8624D1B5680024C2AE /* WebView.swift */; }; 29 | 757C5E8B24D1C9A20024C2AE /* WebViewHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 757C5E8A24D1C9A20024C2AE /* WebViewHolder.swift */; }; 30 | 757C5E8F24D1DFCE0024C2AE /* Common Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 757C5E8E24D1DFCD0024C2AE /* Common Utils.swift */; }; 31 | 75BDFDC324D3F2100041AD20 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75BDFDC224D3F2100041AD20 /* Constants.swift */; }; 32 | 75C2757F24D0A5E500A7A651 /* BookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C2757E24D0A5E500A7A651 /* BookmarksView.swift */; }; 33 | 75C2758324D0AC2C00A7A651 /* ArticleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C2758224D0AC2C00A7A651 /* ArticleViewModel.swift */; }; 34 | 75C2758824D0AEBC00A7A651 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 75C2758724D0AEBC00A7A651 /* SwiftSoup */; }; 35 | 75C2759B24D0BA6B00A7A651 /* Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C2759A24D0BA6B00A7A651 /* Bookmark.swift */; }; 36 | 75C2759D24D0BB6C00A7A651 /* PersistenceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C2759C24D0BB6C00A7A651 /* PersistenceManager.swift */; }; 37 | 75D4FB5325653D7F0095AC82 /* Reachability in Frameworks */ = {isa = PBXBuildFile; productRef = 75D4FB5225653D7F0095AC82 /* Reachability */; }; 38 | 75D4FB5725653DC30095AC82 /* Reachability + Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D4FB5625653DC30095AC82 /* Reachability + Extensions.swift */; }; 39 | 75D4FB5B256542B00095AC82 /* NetworkReachabilty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D4FB5A256542B00095AC82 /* NetworkReachabilty.swift */; }; 40 | 75DDE51824D4097400F9F165 /* NYTimesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75DDE51724D4097400F9F165 /* NYTimesTests.swift */; }; 41 | 75DDE51C24D40DDD00F9F165 /* ArticleRepositoryTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75DDE51B24D40DDD00F9F165 /* ArticleRepositoryTest.swift */; }; 42 | 75F6869924D19C5C003E0E4A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 75F6869824D19C5C003E0E4A /* Kingfisher */; }; 43 | 75F6869B24D19C5C003E0E4A /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 75F6869A24D19C5C003E0E4A /* KingfisherSwiftUI */; }; 44 | /* End PBXBuildFile section */ 45 | 46 | /* Begin PBXContainerItemProxy section */ 47 | 75DDE51224D4094F00F9F165 /* PBXContainerItemProxy */ = { 48 | isa = PBXContainerItemProxy; 49 | containerPortal = 7557FA6F24D0271400F80607 /* Project object */; 50 | proxyType = 1; 51 | remoteGlobalIDString = 7557FA7624D0271400F80607; 52 | remoteInfo = NYTimes; 53 | }; 54 | /* End PBXContainerItemProxy section */ 55 | 56 | /* Begin PBXCopyFilesBuildPhase section */ 57 | 75E694BC24D1988C00D4C04C /* Embed Frameworks */ = { 58 | isa = PBXCopyFilesBuildPhase; 59 | buildActionMask = 2147483647; 60 | dstPath = ""; 61 | dstSubfolderSpec = 10; 62 | files = ( 63 | ); 64 | name = "Embed Frameworks"; 65 | runOnlyForDeploymentPostprocessing = 0; 66 | }; 67 | /* End PBXCopyFilesBuildPhase section */ 68 | 69 | /* Begin PBXFileReference section */ 70 | 732DDA5D27125B650006F018 /* CategoriesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoriesView.swift; sourceTree = ""; }; 71 | 732DDA5F27125B6D0006F018 /* CategoriesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoriesViewModel.swift; sourceTree = ""; }; 72 | 75117C1F24D4294D0097E28F /* BookmarkRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkRepository.swift; sourceTree = ""; }; 73 | 75117C2324D43CE90097E28F /* BookmarkViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkViewModel.swift; sourceTree = ""; }; 74 | 7524922524D053CE00B1ACA5 /* NewsFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsFeedView.swift; sourceTree = ""; }; 75 | 7533BB3624D9E00C00A4AF49 /* HTMLScarperUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLScarperUtility.swift; sourceTree = ""; }; 76 | 754C9A1C24D1500C00706469 /* Article.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Article.swift; sourceTree = ""; }; 77 | 754FAFE624D0308900ACFE9E /* Category.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Category.swift; sourceTree = ""; }; 78 | 7552547024D5E2E700E2143D /* CDArticle+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CDArticle+CoreDataClass.swift"; sourceTree = ""; }; 79 | 7552547124D5E2E700E2143D /* CDArticle+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CDArticle+CoreDataProperties.swift"; sourceTree = ""; }; 80 | 7557FA7724D0271400F80607 /* NYTimes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NYTimes.app; sourceTree = BUILT_PRODUCTS_DIR; }; 81 | 7557FA7A24D0271400F80607 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 82 | 7557FA7C24D0271400F80607 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 83 | 7557FA7F24D0271400F80607 /* NYTimes.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = NYTimes.xcdatamodel; sourceTree = ""; }; 84 | 7557FA8124D0271400F80607 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; 85 | 7557FA8324D0271500F80607 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 86 | 7557FA8624D0271500F80607 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 87 | 7557FA8924D0271500F80607 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 88 | 7557FA8B24D0271500F80607 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 89 | 7557FA9F24D0277200F80607 /* CategorySelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategorySelector.swift; sourceTree = ""; }; 90 | 757C5E8624D1B5680024C2AE /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; 91 | 757C5E8A24D1C9A20024C2AE /* WebViewHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewHolder.swift; sourceTree = ""; }; 92 | 757C5E8E24D1DFCD0024C2AE /* Common Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Common Utils.swift"; sourceTree = ""; }; 93 | 75BDFDC224D3F2100041AD20 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 94 | 75C2757E24D0A5E500A7A651 /* BookmarksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksView.swift; sourceTree = ""; }; 95 | 75C2758224D0AC2C00A7A651 /* ArticleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleViewModel.swift; sourceTree = ""; }; 96 | 75C2759A24D0BA6B00A7A651 /* Bookmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bookmark.swift; sourceTree = ""; }; 97 | 75C2759C24D0BB6C00A7A651 /* PersistenceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceManager.swift; sourceTree = ""; }; 98 | 75D4FB5625653DC30095AC82 /* Reachability + Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Reachability + Extensions.swift"; sourceTree = ""; }; 99 | 75D4FB5A256542B00095AC82 /* NetworkReachabilty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkReachabilty.swift; sourceTree = ""; }; 100 | 75DDE50D24D4094F00F9F165 /* NYTimesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NYTimesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 101 | 75DDE51724D4097400F9F165 /* NYTimesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NYTimesTests.swift; sourceTree = ""; }; 102 | 75DDE51924D4098600F9F165 /* NYTimesTestInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = NYTimesTestInfo.plist; sourceTree = ""; }; 103 | 75DDE51B24D40DDD00F9F165 /* ArticleRepositoryTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleRepositoryTest.swift; sourceTree = ""; }; 104 | /* End PBXFileReference section */ 105 | 106 | /* Begin PBXFrameworksBuildPhase section */ 107 | 7557FA7424D0271400F80607 /* Frameworks */ = { 108 | isa = PBXFrameworksBuildPhase; 109 | buildActionMask = 2147483647; 110 | files = ( 111 | 75F6869B24D19C5C003E0E4A /* KingfisherSwiftUI in Frameworks */, 112 | 75D4FB5325653D7F0095AC82 /* Reachability in Frameworks */, 113 | 75F6869924D19C5C003E0E4A /* Kingfisher in Frameworks */, 114 | 75C2758824D0AEBC00A7A651 /* SwiftSoup in Frameworks */, 115 | ); 116 | runOnlyForDeploymentPostprocessing = 0; 117 | }; 118 | 75DDE50A24D4094F00F9F165 /* Frameworks */ = { 119 | isa = PBXFrameworksBuildPhase; 120 | buildActionMask = 2147483647; 121 | files = ( 122 | ); 123 | runOnlyForDeploymentPostprocessing = 0; 124 | }; 125 | /* End PBXFrameworksBuildPhase section */ 126 | 127 | /* Begin PBXGroup section */ 128 | 732DDA5C27125B4F0006F018 /* Categories */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 732DDA5D27125B650006F018 /* CategoriesView.swift */, 132 | 732DDA5F27125B6D0006F018 /* CategoriesViewModel.swift */, 133 | ); 134 | path = Categories; 135 | sourceTree = ""; 136 | }; 137 | 7533BB3324D9DFE200A4AF49 /* Utilities */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | 757C5E8E24D1DFCD0024C2AE /* Common Utils.swift */, 141 | 7533BB3624D9E00C00A4AF49 /* HTMLScarperUtility.swift */, 142 | 75D4FB5A256542B00095AC82 /* NetworkReachabilty.swift */, 143 | ); 144 | path = Utilities; 145 | sourceTree = ""; 146 | }; 147 | 7557FA6E24D0271400F80607 = { 148 | isa = PBXGroup; 149 | children = ( 150 | 7557FA7924D0271400F80607 /* NYTimes */, 151 | 75DDE50E24D4094F00F9F165 /* NYTimesTests */, 152 | 7557FA7824D0271400F80607 /* Products */, 153 | ); 154 | sourceTree = ""; 155 | }; 156 | 7557FA7824D0271400F80607 /* Products */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | 7557FA7724D0271400F80607 /* NYTimes.app */, 160 | 75DDE50D24D4094F00F9F165 /* NYTimesTests.xctest */, 161 | ); 162 | name = Products; 163 | sourceTree = ""; 164 | }; 165 | 7557FA7924D0271400F80607 /* NYTimes */ = { 166 | isa = PBXGroup; 167 | children = ( 168 | 75D4FB5525653DB40095AC82 /* Extensions */, 169 | 7533BB3324D9DFE200A4AF49 /* Utilities */, 170 | 757C5E9024D1DFDA0024C2AE /* Globals */, 171 | 7557FAA324D0293900F80607 /* Persistence */, 172 | 7557FAA424D0294B00F80607 /* Views */, 173 | 75C2759E24D0BBCC00A7A651 /* Repository */, 174 | 7557FAA224D0292F00F80607 /* ViewModel */, 175 | 7557FAA124D0292600F80607 /* Model */, 176 | 7557FAA524D0295B00F80607 /* Supporting files */, 177 | 7557FA8324D0271500F80607 /* Assets.xcassets */, 178 | 7557FA8B24D0271500F80607 /* Info.plist */, 179 | 7557FA8524D0271500F80607 /* Preview Content */, 180 | ); 181 | path = NYTimes; 182 | sourceTree = ""; 183 | }; 184 | 7557FA8524D0271500F80607 /* Preview Content */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 7557FA8624D0271500F80607 /* Preview Assets.xcassets */, 188 | ); 189 | path = "Preview Content"; 190 | sourceTree = ""; 191 | }; 192 | 7557FAA124D0292600F80607 /* Model */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | 75C2759324D0B3D900A7A651 /* Coredata model */, 196 | 754FAFE624D0308900ACFE9E /* Category.swift */, 197 | 75C2759A24D0BA6B00A7A651 /* Bookmark.swift */, 198 | 754C9A1C24D1500C00706469 /* Article.swift */, 199 | ); 200 | path = Model; 201 | sourceTree = ""; 202 | }; 203 | 7557FAA224D0292F00F80607 /* ViewModel */ = { 204 | isa = PBXGroup; 205 | children = ( 206 | 75C2758224D0AC2C00A7A651 /* ArticleViewModel.swift */, 207 | 75117C2324D43CE90097E28F /* BookmarkViewModel.swift */, 208 | ); 209 | path = ViewModel; 210 | sourceTree = ""; 211 | }; 212 | 7557FAA324D0293900F80607 /* Persistence */ = { 213 | isa = PBXGroup; 214 | children = ( 215 | 7557FA7E24D0271400F80607 /* NYTimes.xcdatamodeld */, 216 | 75C2759C24D0BB6C00A7A651 /* PersistenceManager.swift */, 217 | ); 218 | path = Persistence; 219 | sourceTree = ""; 220 | }; 221 | 7557FAA424D0294B00F80607 /* Views */ = { 222 | isa = PBXGroup; 223 | children = ( 224 | 732DDA5C27125B4F0006F018 /* Categories */, 225 | 7557FA8124D0271400F80607 /* RootView.swift */, 226 | 7557FA9F24D0277200F80607 /* CategorySelector.swift */, 227 | 7524922524D053CE00B1ACA5 /* NewsFeedView.swift */, 228 | 75C2757E24D0A5E500A7A651 /* BookmarksView.swift */, 229 | 757C5E8624D1B5680024C2AE /* WebView.swift */, 230 | 757C5E8A24D1C9A20024C2AE /* WebViewHolder.swift */, 231 | ); 232 | path = Views; 233 | sourceTree = ""; 234 | }; 235 | 7557FAA524D0295B00F80607 /* Supporting files */ = { 236 | isa = PBXGroup; 237 | children = ( 238 | 7557FA8824D0271500F80607 /* LaunchScreen.storyboard */, 239 | 7557FA7A24D0271400F80607 /* AppDelegate.swift */, 240 | 7557FA7C24D0271400F80607 /* SceneDelegate.swift */, 241 | ); 242 | path = "Supporting files"; 243 | sourceTree = ""; 244 | }; 245 | 757C5E9024D1DFDA0024C2AE /* Globals */ = { 246 | isa = PBXGroup; 247 | children = ( 248 | 75BDFDC224D3F2100041AD20 /* Constants.swift */, 249 | ); 250 | path = Globals; 251 | sourceTree = ""; 252 | }; 253 | 75C2759324D0B3D900A7A651 /* Coredata model */ = { 254 | isa = PBXGroup; 255 | children = ( 256 | 7552547024D5E2E700E2143D /* CDArticle+CoreDataClass.swift */, 257 | 7552547124D5E2E700E2143D /* CDArticle+CoreDataProperties.swift */, 258 | ); 259 | path = "Coredata model"; 260 | sourceTree = ""; 261 | }; 262 | 75C2759E24D0BBCC00A7A651 /* Repository */ = { 263 | isa = PBXGroup; 264 | children = ( 265 | 75117C1F24D4294D0097E28F /* BookmarkRepository.swift */, 266 | ); 267 | path = Repository; 268 | sourceTree = ""; 269 | }; 270 | 75D4FB5525653DB40095AC82 /* Extensions */ = { 271 | isa = PBXGroup; 272 | children = ( 273 | 75D4FB5625653DC30095AC82 /* Reachability + Extensions.swift */, 274 | ); 275 | path = Extensions; 276 | sourceTree = ""; 277 | }; 278 | 75DDE50E24D4094F00F9F165 /* NYTimesTests */ = { 279 | isa = PBXGroup; 280 | children = ( 281 | 75DDE51924D4098600F9F165 /* NYTimesTestInfo.plist */, 282 | 75DDE51724D4097400F9F165 /* NYTimesTests.swift */, 283 | 75DDE51B24D40DDD00F9F165 /* ArticleRepositoryTest.swift */, 284 | ); 285 | path = NYTimesTests; 286 | sourceTree = ""; 287 | }; 288 | /* End PBXGroup section */ 289 | 290 | /* Begin PBXNativeTarget section */ 291 | 7557FA7624D0271400F80607 /* NYTimes */ = { 292 | isa = PBXNativeTarget; 293 | buildConfigurationList = 7557FA9924D0271500F80607 /* Build configuration list for PBXNativeTarget "NYTimes" */; 294 | buildPhases = ( 295 | 7557FA7324D0271400F80607 /* Sources */, 296 | 7557FA7424D0271400F80607 /* Frameworks */, 297 | 7557FA7524D0271400F80607 /* Resources */, 298 | 75E694BC24D1988C00D4C04C /* Embed Frameworks */, 299 | ); 300 | buildRules = ( 301 | ); 302 | dependencies = ( 303 | ); 304 | name = NYTimes; 305 | packageProductDependencies = ( 306 | 75C2758724D0AEBC00A7A651 /* SwiftSoup */, 307 | 75F6869824D19C5C003E0E4A /* Kingfisher */, 308 | 75F6869A24D19C5C003E0E4A /* KingfisherSwiftUI */, 309 | 75D4FB5225653D7F0095AC82 /* Reachability */, 310 | ); 311 | productName = NYTimes; 312 | productReference = 7557FA7724D0271400F80607 /* NYTimes.app */; 313 | productType = "com.apple.product-type.application"; 314 | }; 315 | 75DDE50C24D4094F00F9F165 /* NYTimesTests */ = { 316 | isa = PBXNativeTarget; 317 | buildConfigurationList = 75DDE51424D4094F00F9F165 /* Build configuration list for PBXNativeTarget "NYTimesTests" */; 318 | buildPhases = ( 319 | 75DDE50924D4094F00F9F165 /* Sources */, 320 | 75DDE50A24D4094F00F9F165 /* Frameworks */, 321 | 75DDE50B24D4094F00F9F165 /* Resources */, 322 | ); 323 | buildRules = ( 324 | ); 325 | dependencies = ( 326 | 75DDE51324D4094F00F9F165 /* PBXTargetDependency */, 327 | ); 328 | name = NYTimesTests; 329 | productName = NYTimesTests; 330 | productReference = 75DDE50D24D4094F00F9F165 /* NYTimesTests.xctest */; 331 | productType = "com.apple.product-type.bundle.unit-test"; 332 | }; 333 | /* End PBXNativeTarget section */ 334 | 335 | /* Begin PBXProject section */ 336 | 7557FA6F24D0271400F80607 /* Project object */ = { 337 | isa = PBXProject; 338 | attributes = { 339 | LastSwiftUpdateCheck = 1200; 340 | LastUpgradeCheck = 1200; 341 | ORGANIZATIONNAME = "Waseem Akram"; 342 | TargetAttributes = { 343 | 7557FA7624D0271400F80607 = { 344 | CreatedOnToolsVersion = 11.5; 345 | }; 346 | 75DDE50C24D4094F00F9F165 = { 347 | CreatedOnToolsVersion = 12.0; 348 | TestTargetID = 7557FA7624D0271400F80607; 349 | }; 350 | }; 351 | }; 352 | buildConfigurationList = 7557FA7224D0271400F80607 /* Build configuration list for PBXProject "NYTimes" */; 353 | compatibilityVersion = "Xcode 9.3"; 354 | developmentRegion = en; 355 | hasScannedForEncodings = 0; 356 | knownRegions = ( 357 | en, 358 | Base, 359 | ); 360 | mainGroup = 7557FA6E24D0271400F80607; 361 | packageReferences = ( 362 | 75C2758624D0AEBC00A7A651 /* XCRemoteSwiftPackageReference "SwiftSoup" */, 363 | 75F6869724D19C5C003E0E4A /* XCRemoteSwiftPackageReference "Kingfisher" */, 364 | 75D4FB5125653D7F0095AC82 /* XCRemoteSwiftPackageReference "Reachability" */, 365 | ); 366 | productRefGroup = 7557FA7824D0271400F80607 /* Products */; 367 | projectDirPath = ""; 368 | projectRoot = ""; 369 | targets = ( 370 | 7557FA7624D0271400F80607 /* NYTimes */, 371 | 75DDE50C24D4094F00F9F165 /* NYTimesTests */, 372 | ); 373 | }; 374 | /* End PBXProject section */ 375 | 376 | /* Begin PBXResourcesBuildPhase section */ 377 | 7557FA7524D0271400F80607 /* Resources */ = { 378 | isa = PBXResourcesBuildPhase; 379 | buildActionMask = 2147483647; 380 | files = ( 381 | 7557FA8A24D0271500F80607 /* LaunchScreen.storyboard in Resources */, 382 | 7557FA8724D0271500F80607 /* Preview Assets.xcassets in Resources */, 383 | 7557FA8424D0271500F80607 /* Assets.xcassets in Resources */, 384 | ); 385 | runOnlyForDeploymentPostprocessing = 0; 386 | }; 387 | 75DDE50B24D4094F00F9F165 /* Resources */ = { 388 | isa = PBXResourcesBuildPhase; 389 | buildActionMask = 2147483647; 390 | files = ( 391 | ); 392 | runOnlyForDeploymentPostprocessing = 0; 393 | }; 394 | /* End PBXResourcesBuildPhase section */ 395 | 396 | /* Begin PBXSourcesBuildPhase section */ 397 | 7557FA7324D0271400F80607 /* Sources */ = { 398 | isa = PBXSourcesBuildPhase; 399 | buildActionMask = 2147483647; 400 | files = ( 401 | 7557FA8024D0271400F80607 /* NYTimes.xcdatamodeld in Sources */, 402 | 757C5E8724D1B5680024C2AE /* WebView.swift in Sources */, 403 | 754C9A1D24D1500C00706469 /* Article.swift in Sources */, 404 | 75117C2424D43CE90097E28F /* BookmarkViewModel.swift in Sources */, 405 | 7557FA7B24D0271400F80607 /* AppDelegate.swift in Sources */, 406 | 732DDA5E27125B650006F018 /* CategoriesView.swift in Sources */, 407 | 75C2758324D0AC2C00A7A651 /* ArticleViewModel.swift in Sources */, 408 | 75C2759B24D0BA6B00A7A651 /* Bookmark.swift in Sources */, 409 | 75D4FB5B256542B00095AC82 /* NetworkReachabilty.swift in Sources */, 410 | 75C2757F24D0A5E500A7A651 /* BookmarksView.swift in Sources */, 411 | 7557FA8224D0271400F80607 /* RootView.swift in Sources */, 412 | 7557FAA024D0277200F80607 /* CategorySelector.swift in Sources */, 413 | 75D4FB5725653DC30095AC82 /* Reachability + Extensions.swift in Sources */, 414 | 7524922624D053CE00B1ACA5 /* NewsFeedView.swift in Sources */, 415 | 75BDFDC324D3F2100041AD20 /* Constants.swift in Sources */, 416 | 7557FA7D24D0271400F80607 /* SceneDelegate.swift in Sources */, 417 | 757C5E8F24D1DFCE0024C2AE /* Common Utils.swift in Sources */, 418 | 75117C2024D4294D0097E28F /* BookmarkRepository.swift in Sources */, 419 | 7552547224D5E2E700E2143D /* CDArticle+CoreDataClass.swift in Sources */, 420 | 757C5E8B24D1C9A20024C2AE /* WebViewHolder.swift in Sources */, 421 | 7552547324D5E2E700E2143D /* CDArticle+CoreDataProperties.swift in Sources */, 422 | 754FAFE724D0308900ACFE9E /* Category.swift in Sources */, 423 | 75C2759D24D0BB6C00A7A651 /* PersistenceManager.swift in Sources */, 424 | 732DDA6027125B6D0006F018 /* CategoriesViewModel.swift in Sources */, 425 | 7533BB3724D9E00C00A4AF49 /* HTMLScarperUtility.swift in Sources */, 426 | ); 427 | runOnlyForDeploymentPostprocessing = 0; 428 | }; 429 | 75DDE50924D4094F00F9F165 /* Sources */ = { 430 | isa = PBXSourcesBuildPhase; 431 | buildActionMask = 2147483647; 432 | files = ( 433 | 75DDE51C24D40DDD00F9F165 /* ArticleRepositoryTest.swift in Sources */, 434 | 75DDE51824D4097400F9F165 /* NYTimesTests.swift in Sources */, 435 | ); 436 | runOnlyForDeploymentPostprocessing = 0; 437 | }; 438 | /* End PBXSourcesBuildPhase section */ 439 | 440 | /* Begin PBXTargetDependency section */ 441 | 75DDE51324D4094F00F9F165 /* PBXTargetDependency */ = { 442 | isa = PBXTargetDependency; 443 | target = 7557FA7624D0271400F80607 /* NYTimes */; 444 | targetProxy = 75DDE51224D4094F00F9F165 /* PBXContainerItemProxy */; 445 | }; 446 | /* End PBXTargetDependency section */ 447 | 448 | /* Begin PBXVariantGroup section */ 449 | 7557FA8824D0271500F80607 /* LaunchScreen.storyboard */ = { 450 | isa = PBXVariantGroup; 451 | children = ( 452 | 7557FA8924D0271500F80607 /* Base */, 453 | ); 454 | name = LaunchScreen.storyboard; 455 | sourceTree = ""; 456 | }; 457 | /* End PBXVariantGroup section */ 458 | 459 | /* Begin XCBuildConfiguration section */ 460 | 7557FA9724D0271500F80607 /* Debug */ = { 461 | isa = XCBuildConfiguration; 462 | buildSettings = { 463 | ALWAYS_SEARCH_USER_PATHS = NO; 464 | CLANG_ANALYZER_NONNULL = YES; 465 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 466 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 467 | CLANG_CXX_LIBRARY = "libc++"; 468 | CLANG_ENABLE_MODULES = YES; 469 | CLANG_ENABLE_OBJC_ARC = YES; 470 | CLANG_ENABLE_OBJC_WEAK = YES; 471 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 472 | CLANG_WARN_BOOL_CONVERSION = YES; 473 | CLANG_WARN_COMMA = YES; 474 | CLANG_WARN_CONSTANT_CONVERSION = YES; 475 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 476 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 477 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 478 | CLANG_WARN_EMPTY_BODY = YES; 479 | CLANG_WARN_ENUM_CONVERSION = YES; 480 | CLANG_WARN_INFINITE_RECURSION = YES; 481 | CLANG_WARN_INT_CONVERSION = YES; 482 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 483 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 484 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 485 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 486 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 487 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 488 | CLANG_WARN_STRICT_PROTOTYPES = YES; 489 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 490 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 491 | CLANG_WARN_UNREACHABLE_CODE = YES; 492 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 493 | COPY_PHASE_STRIP = NO; 494 | DEBUG_INFORMATION_FORMAT = dwarf; 495 | DISABLE_DIAMOND_PROBLEM_DIAGNOSTIC = 1; 496 | ENABLE_STRICT_OBJC_MSGSEND = YES; 497 | ENABLE_TESTABILITY = YES; 498 | GCC_C_LANGUAGE_STANDARD = gnu11; 499 | GCC_DYNAMIC_NO_PIC = NO; 500 | GCC_NO_COMMON_BLOCKS = YES; 501 | GCC_OPTIMIZATION_LEVEL = 0; 502 | GCC_PREPROCESSOR_DEFINITIONS = ( 503 | "DEBUG=1", 504 | "$(inherited)", 505 | ); 506 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 507 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 508 | GCC_WARN_UNDECLARED_SELECTOR = YES; 509 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 510 | GCC_WARN_UNUSED_FUNCTION = YES; 511 | GCC_WARN_UNUSED_VARIABLE = YES; 512 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 513 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 514 | MTL_FAST_MATH = YES; 515 | ONLY_ACTIVE_ARCH = YES; 516 | SDKROOT = iphoneos; 517 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 518 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 519 | }; 520 | name = Debug; 521 | }; 522 | 7557FA9824D0271500F80607 /* Release */ = { 523 | isa = XCBuildConfiguration; 524 | buildSettings = { 525 | ALWAYS_SEARCH_USER_PATHS = NO; 526 | CLANG_ANALYZER_NONNULL = YES; 527 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 528 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 529 | CLANG_CXX_LIBRARY = "libc++"; 530 | CLANG_ENABLE_MODULES = YES; 531 | CLANG_ENABLE_OBJC_ARC = YES; 532 | CLANG_ENABLE_OBJC_WEAK = YES; 533 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 534 | CLANG_WARN_BOOL_CONVERSION = YES; 535 | CLANG_WARN_COMMA = YES; 536 | CLANG_WARN_CONSTANT_CONVERSION = YES; 537 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 538 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 539 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 540 | CLANG_WARN_EMPTY_BODY = YES; 541 | CLANG_WARN_ENUM_CONVERSION = YES; 542 | CLANG_WARN_INFINITE_RECURSION = YES; 543 | CLANG_WARN_INT_CONVERSION = YES; 544 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 545 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 546 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 547 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 548 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 549 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 550 | CLANG_WARN_STRICT_PROTOTYPES = YES; 551 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 552 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 553 | CLANG_WARN_UNREACHABLE_CODE = YES; 554 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 555 | COPY_PHASE_STRIP = NO; 556 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 557 | DISABLE_DIAMOND_PROBLEM_DIAGNOSTIC = 1; 558 | ENABLE_NS_ASSERTIONS = NO; 559 | ENABLE_STRICT_OBJC_MSGSEND = YES; 560 | GCC_C_LANGUAGE_STANDARD = gnu11; 561 | GCC_NO_COMMON_BLOCKS = YES; 562 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 563 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 564 | GCC_WARN_UNDECLARED_SELECTOR = YES; 565 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 566 | GCC_WARN_UNUSED_FUNCTION = YES; 567 | GCC_WARN_UNUSED_VARIABLE = YES; 568 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 569 | MTL_ENABLE_DEBUG_INFO = NO; 570 | MTL_FAST_MATH = YES; 571 | SDKROOT = iphoneos; 572 | SWIFT_COMPILATION_MODE = wholemodule; 573 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 574 | VALIDATE_PRODUCT = YES; 575 | }; 576 | name = Release; 577 | }; 578 | 7557FA9A24D0271500F80607 /* Debug */ = { 579 | isa = XCBuildConfiguration; 580 | buildSettings = { 581 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 582 | CODE_SIGN_ENTITLEMENTS = ""; 583 | CODE_SIGN_STYLE = Automatic; 584 | DEVELOPMENT_ASSET_PATHS = "\"NYTimes/Preview Content\""; 585 | DEVELOPMENT_TEAM = 7S7EBZGDA6; 586 | ENABLE_PREVIEWS = YES; 587 | INFOPLIST_FILE = NYTimes/Info.plist; 588 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 589 | LD_RUNPATH_SEARCH_PATHS = ( 590 | "$(inherited)", 591 | "@executable_path/Frameworks", 592 | ); 593 | PRODUCT_BUNDLE_IDENTIFIER = com.waseem.NYTimes.example; 594 | PRODUCT_NAME = "$(TARGET_NAME)"; 595 | SUPPORTS_MACCATALYST = NO; 596 | SWIFT_VERSION = 5.0; 597 | TARGETED_DEVICE_FAMILY = "1,2"; 598 | }; 599 | name = Debug; 600 | }; 601 | 7557FA9B24D0271500F80607 /* Release */ = { 602 | isa = XCBuildConfiguration; 603 | buildSettings = { 604 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 605 | CODE_SIGN_ENTITLEMENTS = ""; 606 | CODE_SIGN_STYLE = Automatic; 607 | DEVELOPMENT_ASSET_PATHS = "\"NYTimes/Preview Content\""; 608 | DEVELOPMENT_TEAM = 7S7EBZGDA6; 609 | ENABLE_PREVIEWS = YES; 610 | INFOPLIST_FILE = NYTimes/Info.plist; 611 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 612 | LD_RUNPATH_SEARCH_PATHS = ( 613 | "$(inherited)", 614 | "@executable_path/Frameworks", 615 | ); 616 | PRODUCT_BUNDLE_IDENTIFIER = com.waseem.NYTimes.example; 617 | PRODUCT_NAME = "$(TARGET_NAME)"; 618 | SUPPORTS_MACCATALYST = NO; 619 | SWIFT_VERSION = 5.0; 620 | TARGETED_DEVICE_FAMILY = "1,2"; 621 | }; 622 | name = Release; 623 | }; 624 | 75DDE51524D4094F00F9F165 /* Debug */ = { 625 | isa = XCBuildConfiguration; 626 | buildSettings = { 627 | BUNDLE_LOADER = "$(TEST_HOST)"; 628 | CODE_SIGN_STYLE = Automatic; 629 | DEVELOPMENT_TEAM = Z43AQF953J; 630 | INFOPLIST_FILE = NYTimesTests/NYTimesTestInfo.plist; 631 | LD_RUNPATH_SEARCH_PATHS = ( 632 | "$(inherited)", 633 | "@executable_path/Frameworks", 634 | "@loader_path/Frameworks", 635 | ); 636 | PRODUCT_BUNDLE_IDENTIFIER = com.waseem.NYTimesTests; 637 | PRODUCT_NAME = "$(TARGET_NAME)"; 638 | SWIFT_VERSION = 5.0; 639 | TARGETED_DEVICE_FAMILY = "1,2"; 640 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NYTimes.app/NYTimes"; 641 | }; 642 | name = Debug; 643 | }; 644 | 75DDE51624D4094F00F9F165 /* Release */ = { 645 | isa = XCBuildConfiguration; 646 | buildSettings = { 647 | BUNDLE_LOADER = "$(TEST_HOST)"; 648 | CODE_SIGN_STYLE = Automatic; 649 | DEVELOPMENT_TEAM = Z43AQF953J; 650 | INFOPLIST_FILE = NYTimesTests/NYTimesTestInfo.plist; 651 | LD_RUNPATH_SEARCH_PATHS = ( 652 | "$(inherited)", 653 | "@executable_path/Frameworks", 654 | "@loader_path/Frameworks", 655 | ); 656 | PRODUCT_BUNDLE_IDENTIFIER = com.waseem.NYTimesTests; 657 | PRODUCT_NAME = "$(TARGET_NAME)"; 658 | SWIFT_VERSION = 5.0; 659 | TARGETED_DEVICE_FAMILY = "1,2"; 660 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NYTimes.app/NYTimes"; 661 | }; 662 | name = Release; 663 | }; 664 | /* End XCBuildConfiguration section */ 665 | 666 | /* Begin XCConfigurationList section */ 667 | 7557FA7224D0271400F80607 /* Build configuration list for PBXProject "NYTimes" */ = { 668 | isa = XCConfigurationList; 669 | buildConfigurations = ( 670 | 7557FA9724D0271500F80607 /* Debug */, 671 | 7557FA9824D0271500F80607 /* Release */, 672 | ); 673 | defaultConfigurationIsVisible = 0; 674 | defaultConfigurationName = Release; 675 | }; 676 | 7557FA9924D0271500F80607 /* Build configuration list for PBXNativeTarget "NYTimes" */ = { 677 | isa = XCConfigurationList; 678 | buildConfigurations = ( 679 | 7557FA9A24D0271500F80607 /* Debug */, 680 | 7557FA9B24D0271500F80607 /* Release */, 681 | ); 682 | defaultConfigurationIsVisible = 0; 683 | defaultConfigurationName = Release; 684 | }; 685 | 75DDE51424D4094F00F9F165 /* Build configuration list for PBXNativeTarget "NYTimesTests" */ = { 686 | isa = XCConfigurationList; 687 | buildConfigurations = ( 688 | 75DDE51524D4094F00F9F165 /* Debug */, 689 | 75DDE51624D4094F00F9F165 /* Release */, 690 | ); 691 | defaultConfigurationIsVisible = 0; 692 | defaultConfigurationName = Release; 693 | }; 694 | /* End XCConfigurationList section */ 695 | 696 | /* Begin XCRemoteSwiftPackageReference section */ 697 | 75C2758624D0AEBC00A7A651 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { 698 | isa = XCRemoteSwiftPackageReference; 699 | repositoryURL = "https://github.com/scinfu/SwiftSoup"; 700 | requirement = { 701 | kind = upToNextMajorVersion; 702 | minimumVersion = 2.3.2; 703 | }; 704 | }; 705 | 75D4FB5125653D7F0095AC82 /* XCRemoteSwiftPackageReference "Reachability" */ = { 706 | isa = XCRemoteSwiftPackageReference; 707 | repositoryURL = "https://github.com/ashleymills/Reachability.swift"; 708 | requirement = { 709 | kind = upToNextMajorVersion; 710 | minimumVersion = 5.1.0; 711 | }; 712 | }; 713 | 75F6869724D19C5C003E0E4A /* XCRemoteSwiftPackageReference "Kingfisher" */ = { 714 | isa = XCRemoteSwiftPackageReference; 715 | repositoryURL = "https://github.com/onevcat/Kingfisher"; 716 | requirement = { 717 | kind = upToNextMajorVersion; 718 | minimumVersion = 5.14.1; 719 | }; 720 | }; 721 | /* End XCRemoteSwiftPackageReference section */ 722 | 723 | /* Begin XCSwiftPackageProductDependency section */ 724 | 75C2758724D0AEBC00A7A651 /* SwiftSoup */ = { 725 | isa = XCSwiftPackageProductDependency; 726 | package = 75C2758624D0AEBC00A7A651 /* XCRemoteSwiftPackageReference "SwiftSoup" */; 727 | productName = SwiftSoup; 728 | }; 729 | 75D4FB5225653D7F0095AC82 /* Reachability */ = { 730 | isa = XCSwiftPackageProductDependency; 731 | package = 75D4FB5125653D7F0095AC82 /* XCRemoteSwiftPackageReference "Reachability" */; 732 | productName = Reachability; 733 | }; 734 | 75F6869824D19C5C003E0E4A /* Kingfisher */ = { 735 | isa = XCSwiftPackageProductDependency; 736 | package = 75F6869724D19C5C003E0E4A /* XCRemoteSwiftPackageReference "Kingfisher" */; 737 | productName = Kingfisher; 738 | }; 739 | 75F6869A24D19C5C003E0E4A /* KingfisherSwiftUI */ = { 740 | isa = XCSwiftPackageProductDependency; 741 | package = 75F6869724D19C5C003E0E4A /* XCRemoteSwiftPackageReference "Kingfisher" */; 742 | productName = KingfisherSwiftUI; 743 | }; 744 | /* End XCSwiftPackageProductDependency section */ 745 | 746 | /* Begin XCVersionGroup section */ 747 | 7557FA7E24D0271400F80607 /* NYTimes.xcdatamodeld */ = { 748 | isa = XCVersionGroup; 749 | children = ( 750 | 7557FA7F24D0271400F80607 /* NYTimes.xcdatamodel */, 751 | ); 752 | currentVersion = 7557FA7F24D0271400F80607 /* NYTimes.xcdatamodel */; 753 | path = NYTimes.xcdatamodeld; 754 | sourceTree = ""; 755 | versionGroupType = wrapper.xcdatamodel; 756 | }; 757 | /* End XCVersionGroup section */ 758 | }; 759 | rootObject = 7557FA6F24D0271400F80607 /* Project object */; 760 | } 761 | -------------------------------------------------------------------------------- /NYTimes.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NYTimes.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NYTimes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Kingfisher", 6 | "repositoryURL": "https://github.com/onevcat/Kingfisher", 7 | "state": { 8 | "branch": null, 9 | "revision": "fd6aee6d3774db191b22e331cc88f00041a2ca85", 10 | "version": "5.14.1" 11 | } 12 | }, 13 | { 14 | "package": "Reachability", 15 | "repositoryURL": "https://github.com/ashleymills/Reachability.swift", 16 | "state": { 17 | "branch": null, 18 | "revision": "c01bbdf2d633cf049ae1ed1a68a2020a8bda32e2", 19 | "version": "5.1.0" 20 | } 21 | }, 22 | { 23 | "package": "SwiftSoup", 24 | "repositoryURL": "https://github.com/scinfu/SwiftSoup", 25 | "state": { 26 | "branch": null, 27 | "revision": "774dc9c7213085db8aa59595e27c1cd22e428904", 28 | "version": "2.3.2" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /NYTimes.xcodeproj/project.xcworkspace/xcuserdata/sameernawaz.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes.xcodeproj/project.xcworkspace/xcuserdata/sameernawaz.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /NYTimes.xcodeproj/xcshareddata/xcschemes/NYTimes.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 69 | 70 | 71 | 72 | 78 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /NYTimes.xcodeproj/xcuserdata/sameernawaz.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /NYTimes.xcodeproj/xcuserdata/waseemakram.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 21 | 22 | 36 | 37 | 51 | 52 | 53 | 54 | 55 | 57 | 69 | 70 | 71 | 73 | 83 | 84 | 85 | 87 | 96 | 97 | 98 | 100 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /NYTimes.xcodeproj/xcuserdata/waseemakram.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | NYTimes.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 7557FA7624D0271400F80607 16 | 17 | primary 18 | 19 | 20 | 7557FA8F24D0271500F80607 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/7a60237c47a744133b28a225c53b66cff37952fa/NYTimes/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "29.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "58.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "87.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "80.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "120.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "57.png", 47 | "idiom" : "iphone", 48 | "scale" : "1x", 49 | "size" : "57x57" 50 | }, 51 | { 52 | "filename" : "114.png", 53 | "idiom" : "iphone", 54 | "scale" : "2x", 55 | "size" : "57x57" 56 | }, 57 | { 58 | "filename" : "120.png", 59 | "idiom" : "iphone", 60 | "scale" : "2x", 61 | "size" : "60x60" 62 | }, 63 | { 64 | "filename" : "180.png", 65 | "idiom" : "iphone", 66 | "scale" : "3x", 67 | "size" : "60x60" 68 | }, 69 | { 70 | "filename" : "20.png", 71 | "idiom" : "ipad", 72 | "scale" : "1x", 73 | "size" : "20x20" 74 | }, 75 | { 76 | "filename" : "40.png", 77 | "idiom" : "ipad", 78 | "scale" : "2x", 79 | "size" : "20x20" 80 | }, 81 | { 82 | "filename" : "29.png", 83 | "idiom" : "ipad", 84 | "scale" : "1x", 85 | "size" : "29x29" 86 | }, 87 | { 88 | "filename" : "58.png", 89 | "idiom" : "ipad", 90 | "scale" : "2x", 91 | "size" : "29x29" 92 | }, 93 | { 94 | "filename" : "40.png", 95 | "idiom" : "ipad", 96 | "scale" : "1x", 97 | "size" : "40x40" 98 | }, 99 | { 100 | "filename" : "80.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "40x40" 104 | }, 105 | { 106 | "filename" : "50.png", 107 | "idiom" : "ipad", 108 | "scale" : "1x", 109 | "size" : "50x50" 110 | }, 111 | { 112 | "filename" : "100.png", 113 | "idiom" : "ipad", 114 | "scale" : "2x", 115 | "size" : "50x50" 116 | }, 117 | { 118 | "filename" : "72.png", 119 | "idiom" : "ipad", 120 | "scale" : "1x", 121 | "size" : "72x72" 122 | }, 123 | { 124 | "filename" : "144.png", 125 | "idiom" : "ipad", 126 | "scale" : "2x", 127 | "size" : "72x72" 128 | }, 129 | { 130 | "filename" : "76.png", 131 | "idiom" : "ipad", 132 | "scale" : "1x", 133 | "size" : "76x76" 134 | }, 135 | { 136 | "filename" : "152.png", 137 | "idiom" : "ipad", 138 | "scale" : "2x", 139 | "size" : "76x76" 140 | }, 141 | { 142 | "filename" : "167.png", 143 | "idiom" : "ipad", 144 | "scale" : "2x", 145 | "size" : "83.5x83.5" 146 | }, 147 | { 148 | "filename" : "1024.png", 149 | "idiom" : "ios-marketing", 150 | "scale" : "1x", 151 | "size" : "1024x1024" 152 | }, 153 | { 154 | "filename" : "16.png", 155 | "idiom" : "mac", 156 | "scale" : "1x", 157 | "size" : "16x16" 158 | }, 159 | { 160 | "filename" : "32.png", 161 | "idiom" : "mac", 162 | "scale" : "2x", 163 | "size" : "16x16" 164 | }, 165 | { 166 | "filename" : "32.png", 167 | "idiom" : "mac", 168 | "scale" : "1x", 169 | "size" : "32x32" 170 | }, 171 | { 172 | "filename" : "64.png", 173 | "idiom" : "mac", 174 | "scale" : "2x", 175 | "size" : "32x32" 176 | }, 177 | { 178 | "filename" : "128.png", 179 | "idiom" : "mac", 180 | "scale" : "1x", 181 | "size" : "128x128" 182 | }, 183 | { 184 | "filename" : "256.png", 185 | "idiom" : "mac", 186 | "scale" : "2x", 187 | "size" : "128x128" 188 | }, 189 | { 190 | "filename" : "256.png", 191 | "idiom" : "mac", 192 | "scale" : "1x", 193 | "size" : "256x256" 194 | }, 195 | { 196 | "filename" : "512.png", 197 | "idiom" : "mac", 198 | "scale" : "2x", 199 | "size" : "256x256" 200 | }, 201 | { 202 | "filename" : "512.png", 203 | "idiom" : "mac", 204 | "scale" : "1x", 205 | "size" : "512x512" 206 | }, 207 | { 208 | "filename" : "1024.png", 209 | "idiom" : "mac", 210 | "scale" : "2x", 211 | "size" : "512x512" 212 | } 213 | ], 214 | "info" : { 215 | "author" : "xcode", 216 | "version" : 1 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NYTimes/Assets.xcassets/categoryBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0xFF", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x12", 27 | "green" : "0x12", 28 | "red" : "0x12" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NYTimes/Extensions/Reachability + Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reachability + Extensions.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 18/11/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Reachability 11 | import Combine 12 | 13 | 14 | extension Reachability { 15 | 16 | var onReachabilityChanged: AnyPublisher { 17 | NotificationCenter.default 18 | .publisher(for: .reachabilityChanged, object: self) 19 | .compactMap { 20 | $0.object as? Reachability 21 | } 22 | .eraseToAnyPublisher() 23 | } 24 | 25 | 26 | var status: AnyPublisher { 27 | onReachabilityChanged 28 | .map(\.connection) 29 | .eraseToAnyPublisher() 30 | } 31 | 32 | var isReachable: AnyPublisher { 33 | onReachabilityChanged 34 | .map { $0.connection != .unavailable } 35 | .eraseToAnyPublisher() 36 | } 37 | 38 | var isConnected: AnyPublisher { 39 | isReachable 40 | .filter { $0 } 41 | .map { _ in } 42 | .eraseToAnyPublisher() 43 | } 44 | 45 | var isDisconnected: AnyPublisher { 46 | isReachable 47 | .filter { !$0 } 48 | .map { _ in } 49 | .eraseToAnyPublisher() 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /NYTimes/Globals/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Commons.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 31/07/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Constants { 12 | 13 | class Endpoints { 14 | static let BASEURL = "https://www.nytimes.com" 15 | } 16 | 17 | struct UserDefaults { 18 | static let categories = "UD_CATEGORIES" 19 | } 20 | } 21 | 22 | 23 | class PlaceHolderData { 24 | 25 | static let articles = [ 26 | Article( 27 | url: "https://www.nytimes.com/2020/07/28/science/nasa-jezero-perseverance.html", 28 | imageUrl: "https://static01.nyt.com/images/2020/07/28/science/28SCI-MARS-JEZERO1/28SCI-MARS-JEZERO1-jumbo.jpg", 29 | title: "How NASA Found the Ideal Hole on Mars to Land In", 30 | subtitle: "Jezero crater, the destination of the Perseverance rover, is a promising place to look for evidence of extinct Martian life.", 31 | author: "KENNETH CHANG" 32 | ), 33 | Article( 34 | url: "https://www.nytimes.com/2020/07/28/science/virgin-galactic-cabin.html", 35 | imageUrl: "https://static01.nyt.com/images/2020/07/28/science/28VIRGINGALACTIC2/28VIRGINGALACTIC2-videoLarge.jpg", 36 | title: "Virgin Galactic Unveils Comfy Cabin for Jet-Setting to the Edge of Space", 37 | subtitle: "Passengers able to pay hundreds of thousands of dollars for a seat can escape gravity for a few minutes.", 38 | author: "KENNETH CHANG" 39 | ), 40 | Article( 41 | url: "https://www.nytimes.com/2020/07/28/science/microbes-100-million-years-old.html", 42 | imageUrl: "https://static01.nyt.com/images/2020/07/28/science/28ANCIENT-MICROBES2/28ANCIENT-MICROBES2-videoLarge.jpg", 43 | title: "These Microbes May Have Survived 100 Million Years Beneath the Seafloor", 44 | subtitle: "Rescued from their cold, cramped and nutrient-poor homes, the bacteria awoke in the lab and grew.", 45 | author: "KATHERINE J. WU" 46 | ) 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /NYTimes/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /NYTimes/Model/Article.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Article.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 29/07/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Article:NSObject,Codable,Identifiable { 12 | 13 | public var id: UUID 14 | var url: String 15 | var title: String 16 | var subtitle: String 17 | var author: String 18 | var imageUrl:String 19 | 20 | 21 | init(id: UUID = UUID(),url: String, imageUrl:String,title: String, subtitle: String, author: String) { 22 | self.id = id 23 | self.url = url 24 | self.title = title 25 | self.subtitle = subtitle 26 | self.author = author 27 | self.imageUrl = imageUrl 28 | } 29 | 30 | init(from dataArticle:CDArticle){ 31 | self.id = dataArticle.id 32 | self.url = dataArticle.url 33 | self.title = dataArticle.title 34 | self.subtitle = dataArticle.subtitle 35 | self.author = dataArticle.author 36 | self.imageUrl = dataArticle.imageUrl 37 | } 38 | 39 | 40 | static var placeholder = Article(url: "Url", imageUrl: "imageUrl", title: String(repeating: "Title", count: 5), subtitle: String(repeating: "Subtitle", count: 10), author: String(repeating: "Author", count: 3)) 41 | 42 | } 43 | 44 | -------------------------------------------------------------------------------- /NYTimes/Model/Bookmark.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bookmark.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 29/07/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Bookmark { 12 | var id: UUID 13 | var article:Article 14 | var index: Int16 15 | 16 | init(id: UUID = UUID(), article: Article, index: Int16) { 17 | self.id = id 18 | self.article = article 19 | self.index = index 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NYTimes/Model/Category.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Category.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 28/07/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum Category: String, CaseIterable { 12 | 13 | case tech = "Tech" 14 | case science = "Science" 15 | case business = "Business" 16 | case yourmoney = "Your Money" 17 | case education = "Education" 18 | case sports = "Sports" 19 | case space = "Space" 20 | 21 | var url: String { 22 | switch self { 23 | case .science : return "https://www.nytimes.com/section/science" 24 | case .tech: return "https://www.nytimes.com/section/technology" 25 | case .business: return "https://www.nytimes.com/section/business/smallbusiness" 26 | case .yourmoney: return "https://www.nytimes.com/section/your-money" 27 | case .education: return "https://www.nytimes.com/section/education?module=SiteIndex&pgtype=Section%20Front®ion=Footer" 28 | case .sports: return "https://www.nytimes.com/section/sports/soccer" 29 | case .space: return "https://www.nytimes.com/section/science/space" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /NYTimes/Model/Coredata model/CDArticle+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CDArticle+CoreDataClass.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 01/08/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | // 9 | 10 | import Foundation 11 | import CoreData 12 | 13 | @objc(CDArticle) 14 | public class CDArticle: NSManagedObject { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /NYTimes/Model/Coredata model/CDArticle+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CDArticle+CoreDataProperties.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 01/08/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | // 9 | 10 | import Foundation 11 | import CoreData 12 | 13 | 14 | extension CDArticle { 15 | 16 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 17 | return NSFetchRequest(entityName: "CDArticle") 18 | } 19 | 20 | @NSManaged public var author: String 21 | @NSManaged public var id: UUID 22 | @NSManaged public var imageUrl: String 23 | @NSManaged public var subtitle: String 24 | @NSManaged public var title: String 25 | @NSManaged public var url: String 26 | 27 | 28 | func set(from article: Article){ 29 | author = article.author 30 | id = article.id 31 | imageUrl = article.imageUrl 32 | subtitle = article.subtitle 33 | title = article.title 34 | url = article.url 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /NYTimes/Persistence/NYTimes.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | NYTimes.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /NYTimes/Persistence/NYTimes.xcdatamodeld/NYTimes.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /NYTimes/Persistence/PersistenceManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersistanceManager.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 29/07/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | 11 | /// PersistenceManager is a Singleton which manages the 12 | /// coredata core operations like creating containers, context and handles saving of the data 13 | final class PersistenceManager { 14 | 15 | private init(){} 16 | 17 | static let shared = PersistenceManager() 18 | 19 | lazy var persistentContainer: NSPersistentContainer = { 20 | let container = NSPersistentContainer(name: "NYTimes") 21 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 22 | if let error = error as NSError? { 23 | fatalError("Unresolved error \(error), \(error.userInfo)") 24 | } 25 | }) 26 | return container 27 | }() 28 | 29 | lazy var context = persistentContainer.viewContext 30 | 31 | func saveContext () -> Bool { 32 | let context = persistentContainer.viewContext 33 | if context.hasChanges { 34 | do { 35 | try context.save() 36 | } catch { 37 | let nserror = error as NSError 38 | debugPrint(nserror) 39 | return false 40 | } 41 | return true 42 | } 43 | return false 44 | } 45 | 46 | func fetch(managedObject: T.Type) -> [T]? { 47 | do { 48 | guard let result = try PersistenceManager.shared.context.fetch(managedObject.fetchRequest()) as? [T] else { 49 | return nil 50 | } 51 | 52 | return result 53 | }catch let error { 54 | debugPrint(error) 55 | return nil 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /NYTimes/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NYTimes/Repository/BookmarkRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookmarkRepository.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 31/07/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | 13 | class BookmarkRepository { 14 | 15 | var context:NSManagedObjectContext! 16 | 17 | init(context:NSManagedObjectContext = PersistenceManager.shared.context){ 18 | self.context = context 19 | } 20 | 21 | deinit { 22 | context = nil 23 | } 24 | 25 | func getAll() -> [CDArticle]? { 26 | guard let context = self.context else {return nil} 27 | let results:[CDArticle]? = try? context.fetch(CDArticle.fetchRequest()) 28 | return results 29 | } 30 | 31 | func get(using URL:String)->CDArticle? { 32 | guard let context = self.context else {return nil} 33 | let fetchRequest:NSFetchRequest = CDArticle.fetchRequest() 34 | fetchRequest.predicate = NSPredicate(format: "url == %@", URL) 35 | let results = try? context.fetch(fetchRequest) 36 | return results?.first 37 | } 38 | 39 | func isArticleExists(with URL:String) -> Bool { 40 | guard let context = self.context else {return false} 41 | do { 42 | let fetchRequest:NSFetchRequest = CDArticle.fetchRequest() 43 | fetchRequest.predicate = NSPredicate(format: "url == %@", URL) 44 | let results = try context.fetch(fetchRequest) 45 | if results.first != nil { 46 | return true 47 | } 48 | }catch let error { 49 | debugPrint(error) 50 | } 51 | return false 52 | } 53 | 54 | func insert(article:Article,completion:((Bool)->Void)? = nil) { 55 | guard let context = self.context else { 56 | completion?(false) 57 | return 58 | } 59 | let cdArticle = CDArticle(context: context) 60 | cdArticle.set(from: article) 61 | insert(article: cdArticle) { (result) in 62 | completion?(result) 63 | } 64 | } 65 | 66 | func insert(article: CDArticle, completion:((Bool)->Void)? = nil) { 67 | guard let context = self.context else { 68 | completion?(false) 69 | return 70 | } 71 | 72 | do { 73 | if context.hasChanges { 74 | try context.save() 75 | completion?(true) 76 | return 77 | } 78 | }catch let error { 79 | debugPrint(error) 80 | } 81 | 82 | completion?(false) 83 | } 84 | 85 | func update(article: CDArticle,completion:((Bool)->Void)? = nil) { 86 | guard let context = self.context else { 87 | completion?(false) 88 | return 89 | } 90 | 91 | do { 92 | if context.hasChanges { 93 | try context.save() 94 | completion?(true) 95 | return 96 | } 97 | }catch let error { 98 | debugPrint(error) 99 | } 100 | 101 | completion?(false) 102 | } 103 | 104 | func delete(article: Article,completion: ((Bool)->Void)? = nil) { 105 | guard let context = self.context else { 106 | completion?(false) 107 | return 108 | } 109 | 110 | do { 111 | if let article = get(using: article.url) { 112 | context.delete(article) 113 | try context.save() 114 | completion?(true) 115 | return 116 | } 117 | }catch let error { 118 | debugPrint(error) 119 | } 120 | 121 | completion?(false) 122 | } 123 | 124 | func delete(article: CDArticle,completion: ((Bool)->Void)? = nil) { 125 | guard let context = self.context else { 126 | completion?(false) 127 | return 128 | } 129 | 130 | do { 131 | context.delete(article) 132 | try context.save() 133 | completion?(true) 134 | return 135 | }catch let error { 136 | debugPrint(error) 137 | } 138 | 139 | completion?(false) 140 | } 141 | 142 | func deleteAll() { 143 | guard let context = self.context else {return} 144 | do { 145 | let deleteRequest = NSBatchDeleteRequest(fetchRequest: CDArticle.fetchRequest()) 146 | try context.execute(deleteRequest) 147 | try context.save() 148 | }catch let error { 149 | debugPrint(error) 150 | } 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /NYTimes/Supporting files/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 28/07/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreData 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | // MARK: UISceneSession Lifecycle 23 | 24 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 25 | // Called when a new scene session is being created. 26 | // Use this method to select a configuration to create the new scene with. 27 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 28 | } 29 | 30 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 31 | // Called when the user discards a scene session. 32 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 33 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 34 | } 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /NYTimes/Supporting files/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /NYTimes/Supporting files/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 28/07/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 21 | 22 | // Get the managed object context from the shared persistent container. 23 | let context = PersistenceManager.shared.context 24 | 25 | // Create the SwiftUI view and set the context as the value for the managedObjectContext environment keyPath. 26 | // Add `@Environment(\.managedObjectContext)` in the views that will need the context. 27 | let rootView = RootView().environment(\.managedObjectContext, context) 28 | 29 | // Use a UIHostingController as window root view controller. 30 | if let windowScene = scene as? UIWindowScene { 31 | let window = UIWindow(windowScene: windowScene) 32 | window.rootViewController = UIHostingController(rootView: rootView) 33 | self.window = window 34 | window.makeKeyAndVisible() 35 | } 36 | } 37 | 38 | func sceneDidDisconnect(_ scene: UIScene) { 39 | // Called as the scene is being released by the system. 40 | // This occurs shortly after the scene enters the background, or when its session is discarded. 41 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 42 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 43 | } 44 | 45 | func sceneDidBecomeActive(_ scene: UIScene) { 46 | // Called when the scene has moved from an inactive state to an active state. 47 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 48 | } 49 | 50 | func sceneWillResignActive(_ scene: UIScene) { 51 | // Called when the scene will move from an active state to an inactive state. 52 | // This may occur due to temporary interruptions (ex. an incoming phone call). 53 | } 54 | 55 | func sceneWillEnterForeground(_ scene: UIScene) { 56 | // Called as the scene transitions from the background to the foreground. 57 | // Use this method to undo the changes made on entering the background. 58 | } 59 | 60 | func sceneDidEnterBackground(_ scene: UIScene) { 61 | // Called as the scene transitions from the foreground to the background. 62 | // Use this method to save data, release shared resources, and store enough scene-specific state information 63 | // to restore the scene back to its current state. 64 | 65 | // Save changes in the application's managed object context when the application transitions to the background. 66 | let _ = PersistenceManager.shared.saveContext() 67 | } 68 | 69 | 70 | } 71 | 72 | -------------------------------------------------------------------------------- /NYTimes/Utilities/Common Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 29/07/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | // Phantom type placeholder for undefined methods 13 | func undefined(_ message:String="",file:String=#file,function:String=#function,line: Int=#line) -> T { 14 | fatalError("[File: \(file),Line: \(line),Function: \(function),]: Undefined: \(message)") 15 | } 16 | -------------------------------------------------------------------------------- /NYTimes/Utilities/HTMLScarperUtility.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTMLScarpingUtility.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 04/08/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftSoup 11 | import Combine 12 | 13 | class HTMLScraperUtility { 14 | 15 | func scrapArticle(from data:Data) -> Future<[Article], Never> { 16 | Future { promise in 17 | let html = String(data: data, encoding: .utf8)! 18 | var articles = [Article]() 19 | do { 20 | let elements = try SwiftSoup.parse(html, Constants.Endpoints.BASEURL) 21 | // last updated: 08-20-2023 22 | let documents = try elements.getElementById("stream-panel")?.select("div").select("ol").select("article") 23 | documents?.forEach({ (document) in 24 | let imageUrl = try? document.select("img").attr("src") 25 | let title = try? document.select("h3").text() 26 | let subtitle = try? document.select("p").text() 27 | let author = try? document.select("div").select("p").select("span").text() 28 | let url = try? document.select("a").attr("href") 29 | 30 | if let title = title, 31 | let subtitle = subtitle, 32 | let author = author, 33 | let url = url, 34 | let imageUrl = imageUrl, 35 | !title.isEmpty, 36 | !subtitle.isEmpty, 37 | !author.isEmpty, 38 | !url.isEmpty, 39 | !imageUrl.isEmpty { 40 | 41 | let article = Article(url: "https://www.nytimes.com\(url)", imageUrl: imageUrl, title: title, subtitle: subtitle, author: author) 42 | articles.append(article) 43 | } else { 44 | print("parsing error") 45 | } 46 | }) 47 | promise(.success(articles)) 48 | } catch let error { 49 | debugPrint(error) 50 | promise(.success([])) 51 | return 52 | } 53 | } 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /NYTimes/Utilities/NetworkReachabilty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkReachabilty.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 18/11/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | import Reachability 12 | 13 | class NetworkReachabilty: ObservableObject { 14 | 15 | static let shared = NetworkReachabilty() 16 | 17 | private var cancellable: AnyCancellable? 18 | private var reachability = try? Reachability() 19 | 20 | @Published var isNetworkConnected = true 21 | 22 | private init(){ 23 | guard let reachability = reachability else { return } 24 | cancellable = reachability.isReachable.assign(to: \.isNetworkConnected, on: self) 25 | do { 26 | try reachability.startNotifier() 27 | }catch { 28 | print("Could not start Reachability") 29 | } 30 | } 31 | 32 | private func dispose(){ 33 | cancellable = nil 34 | reachability = nil 35 | } 36 | 37 | deinit { 38 | dispose() 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /NYTimes/ViewModel/ArticleViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsFeedViewModel.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 29/07/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | class ArticleViewModel: ObservableObject { 13 | 14 | var htmlScrapUtlity = HTMLScraperUtility() 15 | 16 | @Published var articles = [Article]() 17 | @Published var isArticlesLoading = false 18 | 19 | var cancellableTask: AnyCancellable? = nil 20 | 21 | func loadArticles(for category: Category) { 22 | guard let url = URL(string: category.url) else { return } 23 | self.isArticlesLoading = true 24 | self.cancellableTask?.cancel() //cancel last subscription to prevent race condition 25 | self.cancellableTask = URLSession.shared.dataTaskPublisher(for: url) 26 | .map(\.data) //extract Data() from tuple 27 | .flatMap(htmlScrapUtlity.scrapArticle(from:)) //send data to scrap function that will return article objects (array) 28 | .sink { (completion) in 29 | self.isArticlesLoading = false //once we got articles, close the loader 30 | } receiveValue: { [unowned self] (articles) in 31 | self.articles = articles 32 | } 33 | } 34 | 35 | deinit { 36 | cancellableTask = nil 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NYTimes/ViewModel/BookmarkViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookmarkViewModel.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 31/07/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | class BookmarkViewModel:ObservableObject { 13 | 14 | var repository:BookmarkRepository! 15 | 16 | @Published var message = String() 17 | @Published var shouldShowAlert = false 18 | 19 | init(repository:BookmarkRepository) { 20 | self.repository = repository 21 | } 22 | 23 | init(){ 24 | 25 | } 26 | 27 | func isArticleExists(with URL:String) -> Bool{ 28 | return repository.isArticleExists(with: URL) 29 | } 30 | 31 | 32 | func bookmark(for article:Article){ 33 | if isArticleExists(with: article.url) { 34 | handleExisitingArticle() 35 | return 36 | } 37 | 38 | repository.insert(article: article) { [weak self] (success) in 39 | guard let self = self else { return } 40 | self.showAlertForAddedBookmark(success: success) 41 | } 42 | } 43 | 44 | func bookmark(for article:CDArticle){ 45 | 46 | if isArticleExists(with: article.url) { 47 | handleExisitingArticle() 48 | return 49 | } 50 | 51 | repository.insert(article: article) { [weak self] (success) in 52 | guard let self = self else { return } 53 | self.showAlertForAddedBookmark(success: success) 54 | } 55 | } 56 | 57 | func deleteBookmark(article:Article, showAlert: Bool = false){ 58 | repository.delete(article: article) { [weak self] (success) in 59 | guard let self = self else { return } 60 | if showAlert { 61 | self.showAlertForDeletedBookmark(success: success) 62 | } 63 | } 64 | } 65 | 66 | func deleteBookmark(article:CDArticle, showAlert: Bool = false){ 67 | repository.delete(article: article) { [weak self] (success) in 68 | guard let self = self else { return } 69 | if showAlert { 70 | self.showAlertForDeletedBookmark(success: success) 71 | } 72 | } 73 | } 74 | 75 | private func handleExisitingArticle() { 76 | self.shouldShowAlert = true 77 | self.message = "Bookmark exists already!" 78 | } 79 | 80 | private func showAlertForAddedBookmark(success:Bool){ 81 | self.shouldShowAlert = true 82 | self.message = success ? "Added to Bookmarks" : "Error Bookmarking this article" 83 | } 84 | 85 | private func showAlertForDeletedBookmark(success:Bool){ 86 | self.shouldShowAlert = true 87 | self.message = success ? "Removed from bookmarks" : "Error deleting this article from bookmarks" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /NYTimes/Views/BookmarksView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookmarksView.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 28/07/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import CoreData 11 | 12 | struct BookmarksView: View { 13 | 14 | @Environment(\.managedObjectContext) var managedObjectContext 15 | 16 | @FetchRequest(entity: CDArticle.entity(), sortDescriptors: [], predicate: nil, animation: Animation.linear) 17 | var savedArticles:FetchedResults 18 | 19 | @ObservedObject var bookmarkViewModel = BookmarkViewModel() 20 | 21 | @ViewBuilder 22 | var body: some View { 23 | List{ 24 | ForEach(savedArticles,id:\.id) { savedArticle in 25 | NavigationLink(destination: WebViewHolder(url: URL(string: savedArticle.url)!, article: Article(from: savedArticle))){ 26 | NewsFeedView(article: Article(from: savedArticle )) 27 | } 28 | } 29 | .onDelete(perform: deleteBookmark(at:)) 30 | } 31 | .listStyle(InsetGroupedListStyle()) 32 | .navigationBarItems(trailing: EditButton()) 33 | .navigationBarTitle("Bookmarks", displayMode: .automatic) 34 | .onAppear { 35 | self.bookmarkViewModel.repository = BookmarkRepository(context: self.managedObjectContext) 36 | } 37 | } 38 | 39 | func deleteBookmark(at offsets:IndexSet){ 40 | for index in offsets { 41 | bookmarkViewModel.deleteBookmark(article: savedArticles[index]) 42 | } 43 | } 44 | } 45 | 46 | struct BookmarksView_Previews: PreviewProvider { 47 | static var previews: some View { 48 | BookmarksView() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /NYTimes/Views/Categories/CategoriesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CategoriesView.swift 3 | // NYTimes 4 | // 5 | // Created by Sameer Nawaz on 09/10/21. 6 | // Copyright © 2021 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct CategoriesView: View { 12 | 13 | @EnvironmentObject var viewModel: CategoriesViewModel 14 | 15 | var body: some View { 16 | List { 17 | Section { 18 | ForEach(viewModel.categories, id: \.rawValue) { category in 19 | Text(category.rawValue) 20 | }.onMove(perform: viewModel.move) 21 | .onDelete(perform: viewModel.delete) 22 | } header: { 23 | Text("My Categories") 24 | } 25 | 26 | Section { 27 | ForEach(viewModel.categoriesUnfollowed, id: \.self) { category in 28 | HStack { 29 | Text(category.rawValue) 30 | Spacer() 31 | Button { 32 | viewModel.addCategory(category) 33 | } label: { 34 | Image(systemName: "plus.circle") 35 | } 36 | 37 | } 38 | } 39 | } header: { 40 | Text("Categories") 41 | } 42 | 43 | } 44 | .toolbar { EditButton() } 45 | .navigationBarTitle("Categories", displayMode: .automatic) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /NYTimes/Views/Categories/CategoriesViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CategoriesViewModel.swift 3 | // NYTimes 4 | // 5 | // Created by Sameer Nawaz on 09/10/21. 6 | // Copyright © 2021 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class CategoriesViewModel: ObservableObject { 12 | 13 | @Published private(set) var categories: [Category] = Category.allCases { 14 | didSet { 15 | saveCategories() 16 | } 17 | } 18 | 19 | @Published var selectedCategory: Category = Category.tech 20 | 21 | @Published var categoriesUnfollowed: [Category] = [] 22 | 23 | init() { 24 | if let categories = UserDefaults.standard 25 | .array(forKey: Constants.UserDefaults.categories) as? [String] { 26 | self.categories = categories.map({ Category(rawValue: $0)! }) 27 | updateUnfollowedCategories() 28 | selectedCategory = self.categories[0] 29 | } 30 | } 31 | 32 | func updateUnfollowedCategories() { 33 | for category in Category.allCases { 34 | if !categories.contains(where: {$0.rawValue == category.rawValue}) { 35 | categoriesUnfollowed.append(category) 36 | } 37 | } 38 | } 39 | 40 | func addCategory(_ category: Category) { 41 | categoriesUnfollowed = categoriesUnfollowed.filter {$0.rawValue != category.rawValue} 42 | categories.append(category) 43 | } 44 | 45 | func delete(at offsets: IndexSet) { 46 | guard let index = offsets.first else { return } 47 | categoriesUnfollowed.append(categories[index]) 48 | categories.remove(atOffsets: offsets) 49 | } 50 | 51 | func move(from source: IndexSet, to destination: Int) { 52 | categories.move(fromOffsets: source, toOffset: destination) 53 | } 54 | 55 | private func saveCategories() { 56 | let categories = categories.map({ $0.rawValue }) 57 | UserDefaults.standard.set(categories, forKey: Constants.UserDefaults.categories) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /NYTimes/Views/CategorySelector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CategorySelector.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 28/07/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct CategorySelector: View { 12 | 13 | @EnvironmentObject var categoriesViewModel: CategoriesViewModel 14 | @EnvironmentObject var articleViewModel: ArticleViewModel 15 | 16 | 17 | var body: some View { 18 | ScrollView(.horizontal, showsIndicators: false) { 19 | HStack { 20 | ForEach(categoriesViewModel.categories, id: \.rawValue) { category in 21 | VStack{ 22 | Spacer() 23 | HStack { 24 | Text(category.rawValue) 25 | .foregroundColor(categoriesViewModel.selectedCategory == category ? .white : .init(UIColor.label)) 26 | .fontWeight(.medium) 27 | } 28 | .padding(.leading,16) 29 | .padding(.trailing,16) 30 | Spacer() 31 | } 32 | .background(categoriesViewModel.selectedCategory == category ? Color.purple : Color("categoryBackground")) 33 | .cornerRadius(10) 34 | .onTapGesture { 35 | categoriesViewModel.selectedCategory = category 36 | articleViewModel.loadArticles(for: category) 37 | } 38 | }.padding(.leading,8) 39 | } 40 | } 41 | .padding(.vertical,24) 42 | .background(Color.init(.secondarySystemBackground)) 43 | .clipped() 44 | .shadow(radius: 15) 45 | } 46 | 47 | } 48 | 49 | 50 | 51 | struct CategorySelector_Previews: PreviewProvider { 52 | static var previews: some View { 53 | CategorySelector() 54 | .environmentObject(ArticleViewModel()) 55 | .environmentObject(CategoriesViewModel()) 56 | .previewDevice(.init("iPhone X")) 57 | 58 | } 59 | } 60 | 61 | 62 | -------------------------------------------------------------------------------- /NYTimes/Views/NewsFeedView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsFeedView.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 28/07/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import KingfisherSwiftUI 11 | 12 | struct NewsFeedView: View { 13 | 14 | var article:Article 15 | 16 | var body: some View { 17 | HStack { 18 | KFImage(URL(string: article.imageUrl)!) 19 | .placeholder({ 20 | ProgressView() 21 | }) 22 | .resizable() 23 | .scaledToFill() 24 | .frame(width: 100, height: 120) 25 | .clipped() 26 | .cornerRadius(12) 27 | 28 | 29 | VStack(alignment: .leading, spacing: 8) { 30 | Text(article.title) 31 | 32 | .font(.headline) 33 | .lineLimit(2) 34 | 35 | Text(article.subtitle) 36 | .font(.subheadline) 37 | .opacity(0.7) 38 | .lineLimit(2) 39 | 40 | Text(article.author) 41 | .font(.system(size: 13, weight: .medium, design: .rounded)) 42 | .multilineTextAlignment(.leading) 43 | }.padding(.horizontal, 12) 44 | } 45 | .padding(12) 46 | } 47 | } 48 | 49 | struct NewsFeedView_Previews: PreviewProvider { 50 | static var previews: some View { 51 | NewsFeedView(article: Article( 52 | url: "https://static01.nyt.com/images/2020/07/28/science/28SCI-MARS-JEZERO1/28SCI-MARS-JEZERO1-jumbo.jpg", 53 | imageUrl: "https://static01.nyt.com/images/2020/07/28/science/28SCI-MARS-JEZERO1/28SCI-MARS-JEZERO1-jumbo.jpg", 54 | title: "How NASA Found the Ideal Hole on Mars to Land In", 55 | subtitle: "Jezero crater, the destination of the Perseverance rover, is a promising place to look for evidence of extinct Martian life.", 56 | author: "KENNETH CHANG" 57 | )) 58 | // .previewDevice(.init(stringLiteral: "iPhone X")) 59 | // .edgesIgnoringSafeArea(.all) 60 | // .environment(\.colorScheme, .dark) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /NYTimes/Views/RootView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 28/07/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import CoreData 11 | 12 | struct RootView: View { 13 | 14 | @Environment(\.managedObjectContext) var managedObjectContext 15 | 16 | @ObservedObject var articlesViewModel = ArticleViewModel() 17 | @ObservedObject var bookmarkViewModel = BookmarkViewModel() 18 | @ObservedObject var networkReachability = NetworkReachabilty.shared 19 | @ObservedObject var categoriesViewModel = CategoriesViewModel() 20 | 21 | @State var shouldShowBookmarks = false 22 | @State var openCategories = false 23 | @State var isLoaded = false 24 | 25 | init() { 26 | articlesViewModel.loadArticles(for: categoriesViewModel.selectedCategory) 27 | } 28 | 29 | var body: some View { 30 | NavigationView { 31 | if networkReachability.isNetworkConnected { 32 | ArticleView() 33 | .edgesIgnoringSafeArea(.bottom) 34 | .navigationViewStyle(StackNavigationViewStyle()) 35 | .navigationBarTitle(Text("NYTimes")) 36 | .navigationBarItems( 37 | leading: categoriesView, 38 | trailing: bookmarksView 39 | ) 40 | } else { 41 | VStack { 42 | Image(systemName: "wifi.slash") 43 | .font(.system(size: 50)) 44 | .frame(width: 50, height: 50, alignment: .center) 45 | .padding(.bottom, 24) 46 | Text("Network not available") 47 | .alert(isPresented: .constant(true)) { 48 | Alert(title: Text("Network not available"), message: Text("Turn on mobile data or use Wi-Fi to access data"), dismissButton: .default(Text("OK"))) 49 | }.navigationBarTitle(Text("NYTimes")) 50 | } 51 | } 52 | }.onAppear { 53 | let repo = BookmarkRepository(context: managedObjectContext) 54 | bookmarkViewModel.repository = repo 55 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(650)) { 56 | isLoaded = true 57 | } 58 | articlesViewModel.loadArticles(for: categoriesViewModel.selectedCategory) 59 | } 60 | } 61 | 62 | private var categoriesView: some View { 63 | Button(action: { openCategories = true }, label: { 64 | Image(systemName: "square.stack.3d.up") 65 | .frame(width: 30, height: 50, alignment: .center) 66 | }) 67 | } 68 | 69 | private var bookmarksView: some View { 70 | Button(action: { shouldShowBookmarks = true }, label: { 71 | Image(systemName: "folder") 72 | .frame(width: 30, height: 50, alignment: .center) 73 | }) 74 | } 75 | 76 | fileprivate func ArticleView() -> some View { 77 | return VStack { 78 | NavigationLink(destination: BookmarksView(), isActive: $shouldShowBookmarks) {} 79 | NavigationLink(destination: CategoriesView().environmentObject(categoriesViewModel), isActive: $openCategories) {} 80 | if articlesViewModel.isArticlesLoading { 81 | VStack{ 82 | Spacer() 83 | ForEach(0..<3) { i in 84 | NewsFeedView(article: .placeholder) 85 | }.redacted(reason: .placeholder) 86 | Spacer() 87 | } 88 | } else { 89 | ArticleListView(articlesViewModel: articlesViewModel, bookmarkViewModel: bookmarkViewModel) 90 | } 91 | Spacer() 92 | CategorySelector() 93 | .environmentObject(articlesViewModel) 94 | .environmentObject(categoriesViewModel) 95 | .frame(height: 100).offset(y: isLoaded ? 0 : 100) 96 | .animation(isLoaded ? .spring() : .none) 97 | } 98 | } 99 | } 100 | 101 | 102 | struct ArticleListView: View { 103 | 104 | @ObservedObject var articlesViewModel: ArticleViewModel 105 | @ObservedObject var bookmarkViewModel: BookmarkViewModel 106 | 107 | var body: some View { 108 | List(articlesViewModel.articles, id: \.id){ article in 109 | NavigationLink(destination: WebViewHolder(url: URL(string: article.url)!, article: article)){ 110 | NewsFeedView(article: article) 111 | .contextMenu(menuItems: { 112 | Button(action: { 113 | bookmarkViewModel.bookmark(for: article) 114 | }) { 115 | Text("Bookmark") 116 | Image(uiImage:UIImage(systemName:"bookmark")!) 117 | } 118 | .alert(isPresented: $bookmarkViewModel.shouldShowAlert) { 119 | return Alert( 120 | title: Text(bookmarkViewModel.message), 121 | dismissButton: .default(Text("OK")) 122 | ) 123 | } 124 | }) 125 | } 126 | } 127 | .listStyle(InsetGroupedListStyle()) 128 | .padding(.bottom, -8) 129 | } 130 | } 131 | 132 | 133 | struct RootView_Previews: PreviewProvider { 134 | static var previews: some View { 135 | Group { 136 | RootView() 137 | .previewDevice(.init(stringLiteral: "iPhone 11")) 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /NYTimes/Views/WebView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebView.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 29/07/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | 10 | import UIKit 11 | import SwiftUI 12 | import WebKit 13 | 14 | struct WebView: UIViewControllerRepresentable { 15 | let url: URL 16 | 17 | func makeUIViewController(context: Context) -> WebviewController { 18 | return WebviewController() 19 | } 20 | 21 | func updateUIViewController(_ webviewController: WebviewController, context: Context) { 22 | var request = URLRequest(url: self.url, cachePolicy: .returnCacheDataElseLoad) 23 | request.httpShouldHandleCookies = false 24 | webviewController.webview.load(request) 25 | } 26 | } 27 | 28 | class WebviewController: UIViewController { 29 | lazy var webview: WKWebView = WKWebView() 30 | lazy var progressbar: UIProgressView = UIProgressView() 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | 35 | self.webview.frame = self.view.frame 36 | self.view.addSubview(self.webview) 37 | 38 | self.view.addSubview(self.progressbar) 39 | self.progressbar.translatesAutoresizingMaskIntoConstraints = false 40 | self.view.addConstraints([ 41 | self.progressbar.topAnchor.constraint(equalTo: self.view.topAnchor), 42 | self.progressbar.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), 43 | self.progressbar.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), 44 | ]) 45 | 46 | self.progressbar.progress = 0.1 47 | webview.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil) 48 | } 49 | 50 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 51 | switch keyPath { 52 | case "estimatedProgress": 53 | if self.webview.estimatedProgress >= 1.0 { 54 | UIView.animate(withDuration: 0.3, animations: { () in 55 | self.progressbar.alpha = 0.0 56 | }, completion: { finished in 57 | self.progressbar.setProgress(0.0, animated: false) 58 | }) 59 | } else { 60 | self.progressbar.isHidden = false 61 | self.progressbar.alpha = 1.0 62 | progressbar.setProgress(Float(self.webview.estimatedProgress), animated: true) 63 | } 64 | default: 65 | super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) 66 | } 67 | } 68 | } 69 | 70 | 71 | -------------------------------------------------------------------------------- /NYTimes/Views/WebViewHolder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebViewHolder.swift 3 | // NYTimes 4 | // 5 | // Created by Waseem Akram on 29/07/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import CoreData 11 | 12 | struct WebViewHolder: View { 13 | 14 | var url:URL 15 | var article:Article 16 | 17 | @State var isBookmarked = false 18 | 19 | @Environment(\.managedObjectContext) var managedObjectContext 20 | 21 | @ObservedObject var bookmarkViewModel = BookmarkViewModel(repository: BookmarkRepository()) 22 | 23 | var body: some View { 24 | WebView(url: url) 25 | .navigationBarItems(trailing: Button(action: { 26 | self.isBookmarked ? bookmarkViewModel.deleteBookmark(article: article,showAlert: true) : bookmarkViewModel.bookmark(for: article) 27 | self.isBookmarked = bookmarkViewModel.isArticleExists(with: article.url) 28 | }, label: { 29 | Image(systemName: "bookmark\(isBookmarked ? ".fill" : "")").frame(width: 30, height: 50,alignment: .center) 30 | }).alert(isPresented: $bookmarkViewModel.shouldShowAlert) { 31 | Alert( 32 | title: Text(bookmarkViewModel.message), 33 | dismissButton: .default(Text("OK")) 34 | ) 35 | }) 36 | .onAppear { 37 | self.isBookmarked = bookmarkViewModel.isArticleExists(with: article.url) 38 | } 39 | } 40 | } 41 | 42 | struct WebViewHolder_Previews: PreviewProvider { 43 | static var previews: some View { 44 | WebViewHolder(url:URL(string: "https://google.com")!, article: PlaceHolderData.articles[0]) 45 | .previewDevice(.init(stringLiteral: "iPhone X")) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /NYTimesTests/ArticleRepositoryTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleRepositoryTest.swift 3 | // NYTimesTests 4 | // 5 | // Created by Waseem Akram on 31/07/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NYTimes 11 | 12 | class ArticleRepositoryTest: XCTestCase { 13 | 14 | var articleRepository: ArticleRepository! 15 | 16 | override func setUp() { 17 | articleRepository = ArticleRepository() 18 | } 19 | 20 | func testScienceArticles(){ 21 | articleRepository.fetchArticles(for: .science) { (articles) in 22 | XCTAssertNotNil(articles) 23 | XCTAssertGreaterThan(articles?.count ?? 0, 0, "Empty science articles") 24 | } 25 | } 26 | 27 | func testSportsArticles(){ 28 | articleRepository.fetchArticles(for: .sports) { (articles) in 29 | XCTAssertNotNil(articles) 30 | XCTAssertGreaterThan(articles?.count ?? 0, 0, "Empty sports articles") 31 | } 32 | } 33 | 34 | func testTechArticles(){ 35 | articleRepository.fetchArticles(for: .tech) { (articles) in 36 | XCTAssertNotNil(articles) 37 | XCTAssertGreaterThan(articles?.count ?? 0, 0, "Empty tech articles") 38 | } 39 | } 40 | 41 | func testBusinessArticles(){ 42 | articleRepository.fetchArticles(for: .business) { (articles) in 43 | XCTAssertNotNil(articles) 44 | XCTAssertGreaterThan(articles?.count ?? 0, 0, "Empty business articles") 45 | } 46 | } 47 | 48 | func testYourMoneyArticles(){ 49 | articleRepository.fetchArticles(for: .yourmoney) { (articles) in 50 | XCTAssertNotNil(articles) 51 | XCTAssertGreaterThan(articles?.count ?? 0, 0, "Empty your money articles") 52 | } 53 | } 54 | 55 | func testEducationArticles(){ 56 | articleRepository.fetchArticles(for: .education) { (articles) in 57 | XCTAssertNotNil(articles) 58 | XCTAssertGreaterThan(articles?.count ?? 0, 0, "Empty education articles") 59 | } 60 | } 61 | 62 | func testSpaceArticles(){ 63 | articleRepository.fetchArticles(for: .space) { (articles) in 64 | XCTAssertNotNil(articles) 65 | XCTAssertGreaterThan(articles?.count ?? 0, 0, "Empty space articles") 66 | } 67 | } 68 | 69 | override func tearDown() { 70 | articleRepository = nil 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /NYTimesTests/NYTimesTestInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /NYTimesTests/NYTimesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NYTimesTests.swift 3 | // NYTimesTests 4 | // 5 | // Created by Waseem Akram on 31/07/20. 6 | // Copyright © 2020 Waseem Akram. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NYTimes 11 | 12 | 13 | class NYTimesTests: XCTestCase { 14 | 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/master/NYTimes%20Screenshots/Banner%400.25x.png) 2 | ![SwiftUI](https://img.shields.io/badge/Interface-SwfitUI-red) 3 | ![Architecture](https://img.shields.io/badge/Architecture-MVVM-green) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/TheCodeMonks/NYTimes-iOS/blob/master/LICENSE) 5 | ![GitHub forks](https://img.shields.io/github/forks/TheCodeMonks/NYTimes-iOS?label=Fork&style=social) 6 | ![GitHub Stars](https://img.shields.io/github/stars/TheCodeMonks/NYTimes-iOS?label=Stars&style=social) 7 | ![GitHub Watchers](https://img.shields.io/github/watchers/TheCodeMonks/NYTimes-iOS?label=Watchers&style=social) 8 | 9 | 10 | [![Twitter URL](https://img.shields.io/twitter/url?style=social&url=https://github.com/TheCodeMonks/NYTimes-iOS)](http://twitter.com/share?text=Checkout+this+cool+project+made+with+SwiftUI,+by+@iamwaseem99&url=https://github.com/TheCodeMonks/NYTimes-iOS&hashtags=swiftui,ios,iphone,news,github,iosdevelopers,swift,xcode) 11 | [![Twitter Follow](https://img.shields.io/twitter/follow/iamwaseem99?style=social)](https://twitter.com/iamwaseem99) 12 | 13 | 14 | 15 | # NYTimes 16 | 17 | NY Times is an Minimal News 🗞 iOS application built to describe the use of **SwiftSoup** and **CoreData** with **SwiftUI**. 18 | 19 | ## ⛓ Features 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
Articles Loading Browse by Category
Bookmark a Article using 3D Touch. Deleting bookmarks is just one swipe away
39 | 40 | ## 📝 Table of Contents 41 | - [Requirements](#requirements) 42 | - [What you can learn?](#whatyoucanlearn) 43 | - [Technical Background](#techbackground) 44 | - [Dependencies](#dependencies) 45 | - [Project Structure](#projectstructure) 46 | - [Features](#features) 47 | - [Contribute](#contribute) 48 | - [Contact](#contact) 49 | - [License](#license) 50 | 51 | 52 | 53 | 54 | 55 | 56 | ## ⚙️ Requirements 57 | ``` 58 | iOS 14+ 59 | Xcode 12.2 and Up 60 | ``` 61 | 62 | 63 | 64 | ## 📚 What you can learn? 65 | - You can learn Technologies like 66 | - SwiftUI 67 | - CoreData 68 | - Combine 69 | - Web Scraping 70 | - You can learn various Design patterns from this project such as 71 | - Dependency injection 72 | - Repository 73 | - Singleton 74 | - Observers 75 | - You can learn MVVM Two way binding Architecture for SwiftUI with Combine framework 76 | 77 | 78 | 79 | ## 🛠 Technical Background 80 | - NYTimes App was made using SwiftUI as the Core interface with Two Way Binding MVVM Architecture using Combine framework. 81 | - CoreData is used to store the Article Bookmarks offline in device so that the user can access it at later time. 82 | - SwiftSoup is used to scrap the required details from the NYTimes website. 83 | - The User interface of this app mostly uses the inbuilt iOS components to keep the User experience close to the native feel. 84 | - Bookmarks can be added as easy as a 3d-touch from Homescreen or a tap in bookmark icon in the detailed article screen. 85 | - This project was built in the mindset of modularity and good coding patterns. Multiple design patterns like Dependency injection, Repository pattern, Singleton Pattern etc. 86 | 87 | 88 | 89 | ## 🔗 Dependencies 90 | 91 | This project uses SPM (Swift Package Manager) as Dependency manager. 92 | 93 | - [SwiftSoup](https://github.com/scinfu/SwiftSoup) 94 | - [Kingfisher](https://github.com/onevcat/Kingfisher) 95 | - [Reachability](https://github.com/ashleymills/Reachability.swift) 96 | 97 | 98 | 99 | ## ⛓ Project Structure 100 | 101 | NYTimes # Root Group 102 | . 103 | ├── Utilities # Utilities for Fetching data ans Scraping HTML 104 | ├── Extensions # Some useful extensions 105 | ├── Globals # Contains App constants 106 | ├── Persistence # Coredata files. Contains coredata model and Singleton for ManagedObjectContext 107 | ├── Views # SwiftUI Views 108 | ├── Repository # Repository for Coredata 109 | ├── ViewModel # Viewmodels for SwiftUI Views 110 | ├── Model # Model files 111 | | └── Coredata Model # Coredata model subclasses 112 | | 113 | └── Supporting files # Misc. files like Appdelegate, SceneDelegate. 114 | 115 | ## Architecture 116 | 117 | This app uses MVVM architecture. 118 | 119 | ![MVVM](https://raw.githubusercontent.com/TheCodeMonks/NYTimes-iOS/master/NYTimes%20Screenshots/MVVM.jpeg) 120 | 121 | 122 | 123 | 124 | ## ✏️ Contribute 125 | 126 | If you want to contribute to this library, you're always welcome! 127 | 128 | ### What you can do 129 | You can contribute us by filing issues, bugs and PRs. 130 | 131 | ### Before you do 132 | Before you open a issue or report a bug, please check if the issue or bug is related to Xcode or SwiftUI. 133 | 134 | ### Contributing guidelines: 135 | - Open issue regarding proposed change. 136 | - Repo owner will contact you there. 137 | - If your proposed change is approved, Fork this repo and do changes. 138 | - Open PR against latest `dev` branch. Add nice description in PR. 139 | - You're done! 140 | 141 | ## ☕️ Donation 142 | If this project help you reduce time to develop, you can give me a cup of coffee :) 143 | 144 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.me/iamwaseem99) 145 | 146 | 147 | 148 | ## 📱 Contact 149 | 150 | Have an project? DM us at 👇 151 | 152 | Drop a mail to:- thecodemonksorg@gmail.com 153 | 154 | 155 | 156 | ## ⚖️ [License](https://github.com/TheCodeMonks/NYTimes-iOS/blob/master/LICENSE) 157 | 158 | ``` 159 | MIT License 160 | 161 | Copyright (c) 2020 TheCodeMonks 162 | 163 | Permission is hereby granted, free of charge, to any person obtaining a copy 164 | of this software and associated documentation files (the "Software"), to deal 165 | in the Software without restriction, including without limitation the rights 166 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 167 | copies of the Software, and to permit persons to whom the Software is 168 | furnished to do so, subject to the following conditions: 169 | 170 | The above copyright notice and this permission notice shall be included in all 171 | copies or substantial portions of the Software. 172 | 173 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 174 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 175 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 176 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 177 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 178 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 179 | SOFTWARE. 180 | ``` 181 | --------------------------------------------------------------------------------