├── README.md ├── TraktTV.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── majid.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── TraktTV ├── AppDelegate.swift ├── Assets.xcassets │ ├── App Icon & Top Shelf Image.brandassets │ │ ├── App Icon - App Store.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ └── Middle.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ ├── App Icon.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ └── Middle.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Top Shelf Image Wide.imageset │ │ │ └── Contents.json │ │ └── Top Shelf Image.imageset │ │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── ContentView.swift ├── Features │ ├── Networking │ │ ├── Extensions │ │ │ ├── URL.swift │ │ │ └── URLQueryItem.swift │ │ ├── Logic │ │ │ └── DataLoader.swift │ │ └── Models │ │ │ ├── Endpoint.swift │ │ │ ├── Request.swift │ │ │ └── RequestModifier.swift │ └── Search │ │ ├── Logic │ │ ├── ImageService.swift │ │ ├── SearchService.swift │ │ └── SearchStore.swift │ │ ├── Model │ │ ├── Endpoint+Images.swift │ │ ├── Endpoint+Search.swift │ │ ├── Images.swift │ │ └── Show.swift │ │ └── Views │ │ ├── PosterView.swift │ │ └── ShelfView.swift ├── Info.plist └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json └── screenshot.jpg /README.md: -------------------------------------------------------------------------------- 1 | # swiftui-trakttv-app 2 | Simple tvOS app that shows trending shows written in SwiftUI 3 | 4 | ![Screenshot](screenshot.jpg) 5 | -------------------------------------------------------------------------------- /TraktTV.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1C53B54D244DD38F00B044C0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C53B54C244DD38F00B044C0 /* AppDelegate.swift */; }; 11 | 1C53B54F244DD38F00B044C0 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C53B54E244DD38F00B044C0 /* ContentView.swift */; }; 12 | 1C53B551244DD39000B044C0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1C53B550244DD39000B044C0 /* Assets.xcassets */; }; 13 | 1C53B554244DD39000B044C0 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1C53B553244DD39000B044C0 /* Preview Assets.xcassets */; }; 14 | 1C53B557244DD39000B044C0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1C53B555244DD39000B044C0 /* LaunchScreen.storyboard */; }; 15 | 1C53B56A244DD3CE00B044C0 /* DataLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C53B562244DD3CE00B044C0 /* DataLoader.swift */; }; 16 | 1C53B56B244DD3CE00B044C0 /* RequestModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C53B564244DD3CE00B044C0 /* RequestModifier.swift */; }; 17 | 1C53B56C244DD3CE00B044C0 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C53B565244DD3CE00B044C0 /* Request.swift */; }; 18 | 1C53B56D244DD3CE00B044C0 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C53B566244DD3CE00B044C0 /* Endpoint.swift */; }; 19 | 1C53B56E244DD3CE00B044C0 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C53B568244DD3CE00B044C0 /* URL.swift */; }; 20 | 1C53B56F244DD3CE00B044C0 /* URLQueryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C53B569244DD3CE00B044C0 /* URLQueryItem.swift */; }; 21 | 1C53B575244DD48A00B044C0 /* SearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C53B574244DD48A00B044C0 /* SearchService.swift */; }; 22 | 1C53B577244DD4A100B044C0 /* Endpoint+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C53B576244DD4A100B044C0 /* Endpoint+Search.swift */; }; 23 | 1C53B57B244DD51A00B044C0 /* Show.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C53B57A244DD51A00B044C0 /* Show.swift */; }; 24 | 1C53B57D244DD59A00B044C0 /* SearchStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C53B57C244DD59A00B044C0 /* SearchStore.swift */; }; 25 | 1C53B583244DD70300B044C0 /* Endpoint+Images.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C53B582244DD70300B044C0 /* Endpoint+Images.swift */; }; 26 | 1C53B585244DD71C00B044C0 /* Images.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C53B584244DD71B00B044C0 /* Images.swift */; }; 27 | 1C53B588244E0F2E00B044C0 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 1C53B587244E0F2E00B044C0 /* KingfisherSwiftUI */; }; 28 | 1C7BDF51244F237F003880CE /* ImageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7BDF50244F237F003880CE /* ImageService.swift */; }; 29 | 1CCD168B244F1A84004AD4CE /* PosterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CCD168A244F1A84004AD4CE /* PosterView.swift */; }; 30 | 1CCD168D244F1CED004AD4CE /* ShelfView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CCD168C244F1CED004AD4CE /* ShelfView.swift */; }; 31 | /* End PBXBuildFile section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | 1C53B549244DD38F00B044C0 /* TraktTV.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TraktTV.app; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | 1C53B54C244DD38F00B044C0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 36 | 1C53B54E244DD38F00B044C0 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 37 | 1C53B550244DD39000B044C0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 38 | 1C53B553244DD39000B044C0 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 39 | 1C53B556244DD39000B044C0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 40 | 1C53B558244DD39000B044C0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 41 | 1C53B562244DD3CE00B044C0 /* DataLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataLoader.swift; sourceTree = ""; }; 42 | 1C53B564244DD3CE00B044C0 /* RequestModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestModifier.swift; sourceTree = ""; }; 43 | 1C53B565244DD3CE00B044C0 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; 44 | 1C53B566244DD3CE00B044C0 /* Endpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = ""; }; 45 | 1C53B568244DD3CE00B044C0 /* URL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; 46 | 1C53B569244DD3CE00B044C0 /* URLQueryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLQueryItem.swift; sourceTree = ""; }; 47 | 1C53B574244DD48A00B044C0 /* SearchService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchService.swift; sourceTree = ""; }; 48 | 1C53B576244DD4A100B044C0 /* Endpoint+Search.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Endpoint+Search.swift"; sourceTree = ""; }; 49 | 1C53B57A244DD51A00B044C0 /* Show.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Show.swift; sourceTree = ""; }; 50 | 1C53B57C244DD59A00B044C0 /* SearchStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchStore.swift; sourceTree = ""; }; 51 | 1C53B582244DD70300B044C0 /* Endpoint+Images.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Endpoint+Images.swift"; sourceTree = ""; }; 52 | 1C53B584244DD71B00B044C0 /* Images.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Images.swift; sourceTree = ""; }; 53 | 1C7BDF50244F237F003880CE /* ImageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageService.swift; sourceTree = ""; }; 54 | 1CCD168A244F1A84004AD4CE /* PosterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterView.swift; sourceTree = ""; }; 55 | 1CCD168C244F1CED004AD4CE /* ShelfView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShelfView.swift; sourceTree = ""; }; 56 | /* End PBXFileReference section */ 57 | 58 | /* Begin PBXFrameworksBuildPhase section */ 59 | 1C53B546244DD38F00B044C0 /* Frameworks */ = { 60 | isa = PBXFrameworksBuildPhase; 61 | buildActionMask = 2147483647; 62 | files = ( 63 | 1C53B588244E0F2E00B044C0 /* KingfisherSwiftUI in Frameworks */, 64 | ); 65 | runOnlyForDeploymentPostprocessing = 0; 66 | }; 67 | /* End PBXFrameworksBuildPhase section */ 68 | 69 | /* Begin PBXGroup section */ 70 | 1C53B540244DD38F00B044C0 = { 71 | isa = PBXGroup; 72 | children = ( 73 | 1C53B54B244DD38F00B044C0 /* TraktTV */, 74 | 1C53B54A244DD38F00B044C0 /* Products */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | 1C53B54A244DD38F00B044C0 /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 1C53B549244DD38F00B044C0 /* TraktTV.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 1C53B54B244DD38F00B044C0 /* TraktTV */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 1C53B55F244DD3A600B044C0 /* Features */, 90 | 1C53B54C244DD38F00B044C0 /* AppDelegate.swift */, 91 | 1C53B54E244DD38F00B044C0 /* ContentView.swift */, 92 | 1C53B550244DD39000B044C0 /* Assets.xcassets */, 93 | 1C53B555244DD39000B044C0 /* LaunchScreen.storyboard */, 94 | 1C53B558244DD39000B044C0 /* Info.plist */, 95 | 1C53B552244DD39000B044C0 /* Preview Content */, 96 | ); 97 | path = TraktTV; 98 | sourceTree = ""; 99 | }; 100 | 1C53B552244DD39000B044C0 /* Preview Content */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 1C53B553244DD39000B044C0 /* Preview Assets.xcassets */, 104 | ); 105 | path = "Preview Content"; 106 | sourceTree = ""; 107 | }; 108 | 1C53B55F244DD3A600B044C0 /* Features */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | 1C53B570244DD45200B044C0 /* Search */, 112 | 1C53B560244DD3CE00B044C0 /* Networking */, 113 | ); 114 | path = Features; 115 | sourceTree = ""; 116 | }; 117 | 1C53B560244DD3CE00B044C0 /* Networking */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | 1C53B561244DD3CE00B044C0 /* Logic */, 121 | 1C53B563244DD3CE00B044C0 /* Models */, 122 | 1C53B567244DD3CE00B044C0 /* Extensions */, 123 | ); 124 | path = Networking; 125 | sourceTree = ""; 126 | }; 127 | 1C53B561244DD3CE00B044C0 /* Logic */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | 1C53B562244DD3CE00B044C0 /* DataLoader.swift */, 131 | ); 132 | path = Logic; 133 | sourceTree = ""; 134 | }; 135 | 1C53B563244DD3CE00B044C0 /* Models */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | 1C53B564244DD3CE00B044C0 /* RequestModifier.swift */, 139 | 1C53B565244DD3CE00B044C0 /* Request.swift */, 140 | 1C53B566244DD3CE00B044C0 /* Endpoint.swift */, 141 | ); 142 | path = Models; 143 | sourceTree = ""; 144 | }; 145 | 1C53B567244DD3CE00B044C0 /* Extensions */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | 1C53B568244DD3CE00B044C0 /* URL.swift */, 149 | 1C53B569244DD3CE00B044C0 /* URLQueryItem.swift */, 150 | ); 151 | path = Extensions; 152 | sourceTree = ""; 153 | }; 154 | 1C53B570244DD45200B044C0 /* Search */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | 1C53B573244DD46600B044C0 /* Views */, 158 | 1C53B572244DD46000B044C0 /* Model */, 159 | 1C53B571244DD45900B044C0 /* Logic */, 160 | ); 161 | path = Search; 162 | sourceTree = ""; 163 | }; 164 | 1C53B571244DD45900B044C0 /* Logic */ = { 165 | isa = PBXGroup; 166 | children = ( 167 | 1C53B574244DD48A00B044C0 /* SearchService.swift */, 168 | 1C53B57C244DD59A00B044C0 /* SearchStore.swift */, 169 | 1C7BDF50244F237F003880CE /* ImageService.swift */, 170 | ); 171 | path = Logic; 172 | sourceTree = ""; 173 | }; 174 | 1C53B572244DD46000B044C0 /* Model */ = { 175 | isa = PBXGroup; 176 | children = ( 177 | 1C53B584244DD71B00B044C0 /* Images.swift */, 178 | 1C53B57A244DD51A00B044C0 /* Show.swift */, 179 | 1C53B576244DD4A100B044C0 /* Endpoint+Search.swift */, 180 | 1C53B582244DD70300B044C0 /* Endpoint+Images.swift */, 181 | ); 182 | path = Model; 183 | sourceTree = ""; 184 | }; 185 | 1C53B573244DD46600B044C0 /* Views */ = { 186 | isa = PBXGroup; 187 | children = ( 188 | 1CCD168A244F1A84004AD4CE /* PosterView.swift */, 189 | 1CCD168C244F1CED004AD4CE /* ShelfView.swift */, 190 | ); 191 | path = Views; 192 | sourceTree = ""; 193 | }; 194 | /* End PBXGroup section */ 195 | 196 | /* Begin PBXNativeTarget section */ 197 | 1C53B548244DD38F00B044C0 /* TraktTV */ = { 198 | isa = PBXNativeTarget; 199 | buildConfigurationList = 1C53B55B244DD39000B044C0 /* Build configuration list for PBXNativeTarget "TraktTV" */; 200 | buildPhases = ( 201 | 1C53B545244DD38F00B044C0 /* Sources */, 202 | 1C53B546244DD38F00B044C0 /* Frameworks */, 203 | 1C53B547244DD38F00B044C0 /* Resources */, 204 | ); 205 | buildRules = ( 206 | ); 207 | dependencies = ( 208 | ); 209 | name = TraktTV; 210 | packageProductDependencies = ( 211 | 1C53B587244E0F2E00B044C0 /* KingfisherSwiftUI */, 212 | ); 213 | productName = TraktTV; 214 | productReference = 1C53B549244DD38F00B044C0 /* TraktTV.app */; 215 | productType = "com.apple.product-type.application"; 216 | }; 217 | /* End PBXNativeTarget section */ 218 | 219 | /* Begin PBXProject section */ 220 | 1C53B541244DD38F00B044C0 /* Project object */ = { 221 | isa = PBXProject; 222 | attributes = { 223 | LastSwiftUpdateCheck = 1140; 224 | LastUpgradeCheck = 1140; 225 | ORGANIZATIONNAME = "Majid Jabrayilov"; 226 | TargetAttributes = { 227 | 1C53B548244DD38F00B044C0 = { 228 | CreatedOnToolsVersion = 11.4.1; 229 | }; 230 | }; 231 | }; 232 | buildConfigurationList = 1C53B544244DD38F00B044C0 /* Build configuration list for PBXProject "TraktTV" */; 233 | compatibilityVersion = "Xcode 9.3"; 234 | developmentRegion = en; 235 | hasScannedForEncodings = 0; 236 | knownRegions = ( 237 | en, 238 | Base, 239 | ); 240 | mainGroup = 1C53B540244DD38F00B044C0; 241 | packageReferences = ( 242 | 1C53B586244E0F2E00B044C0 /* XCRemoteSwiftPackageReference "Kingfisher" */, 243 | ); 244 | productRefGroup = 1C53B54A244DD38F00B044C0 /* Products */; 245 | projectDirPath = ""; 246 | projectRoot = ""; 247 | targets = ( 248 | 1C53B548244DD38F00B044C0 /* TraktTV */, 249 | ); 250 | }; 251 | /* End PBXProject section */ 252 | 253 | /* Begin PBXResourcesBuildPhase section */ 254 | 1C53B547244DD38F00B044C0 /* Resources */ = { 255 | isa = PBXResourcesBuildPhase; 256 | buildActionMask = 2147483647; 257 | files = ( 258 | 1C53B557244DD39000B044C0 /* LaunchScreen.storyboard in Resources */, 259 | 1C53B554244DD39000B044C0 /* Preview Assets.xcassets in Resources */, 260 | 1C53B551244DD39000B044C0 /* Assets.xcassets in Resources */, 261 | ); 262 | runOnlyForDeploymentPostprocessing = 0; 263 | }; 264 | /* End PBXResourcesBuildPhase section */ 265 | 266 | /* Begin PBXSourcesBuildPhase section */ 267 | 1C53B545244DD38F00B044C0 /* Sources */ = { 268 | isa = PBXSourcesBuildPhase; 269 | buildActionMask = 2147483647; 270 | files = ( 271 | 1C53B577244DD4A100B044C0 /* Endpoint+Search.swift in Sources */, 272 | 1CCD168D244F1CED004AD4CE /* ShelfView.swift in Sources */, 273 | 1C53B575244DD48A00B044C0 /* SearchService.swift in Sources */, 274 | 1C53B585244DD71C00B044C0 /* Images.swift in Sources */, 275 | 1CCD168B244F1A84004AD4CE /* PosterView.swift in Sources */, 276 | 1C53B56B244DD3CE00B044C0 /* RequestModifier.swift in Sources */, 277 | 1C53B57D244DD59A00B044C0 /* SearchStore.swift in Sources */, 278 | 1C53B54F244DD38F00B044C0 /* ContentView.swift in Sources */, 279 | 1C53B56C244DD3CE00B044C0 /* Request.swift in Sources */, 280 | 1C53B57B244DD51A00B044C0 /* Show.swift in Sources */, 281 | 1C53B56E244DD3CE00B044C0 /* URL.swift in Sources */, 282 | 1C53B54D244DD38F00B044C0 /* AppDelegate.swift in Sources */, 283 | 1C53B56F244DD3CE00B044C0 /* URLQueryItem.swift in Sources */, 284 | 1C7BDF51244F237F003880CE /* ImageService.swift in Sources */, 285 | 1C53B56D244DD3CE00B044C0 /* Endpoint.swift in Sources */, 286 | 1C53B583244DD70300B044C0 /* Endpoint+Images.swift in Sources */, 287 | 1C53B56A244DD3CE00B044C0 /* DataLoader.swift in Sources */, 288 | ); 289 | runOnlyForDeploymentPostprocessing = 0; 290 | }; 291 | /* End PBXSourcesBuildPhase section */ 292 | 293 | /* Begin PBXVariantGroup section */ 294 | 1C53B555244DD39000B044C0 /* LaunchScreen.storyboard */ = { 295 | isa = PBXVariantGroup; 296 | children = ( 297 | 1C53B556244DD39000B044C0 /* Base */, 298 | ); 299 | name = LaunchScreen.storyboard; 300 | sourceTree = ""; 301 | }; 302 | /* End PBXVariantGroup section */ 303 | 304 | /* Begin XCBuildConfiguration section */ 305 | 1C53B559244DD39000B044C0 /* Debug */ = { 306 | isa = XCBuildConfiguration; 307 | buildSettings = { 308 | ALWAYS_SEARCH_USER_PATHS = NO; 309 | CLANG_ANALYZER_NONNULL = YES; 310 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 311 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 312 | CLANG_CXX_LIBRARY = "libc++"; 313 | CLANG_ENABLE_MODULES = YES; 314 | CLANG_ENABLE_OBJC_ARC = YES; 315 | CLANG_ENABLE_OBJC_WEAK = YES; 316 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 317 | CLANG_WARN_BOOL_CONVERSION = YES; 318 | CLANG_WARN_COMMA = YES; 319 | CLANG_WARN_CONSTANT_CONVERSION = YES; 320 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 321 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 322 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 323 | CLANG_WARN_EMPTY_BODY = YES; 324 | CLANG_WARN_ENUM_CONVERSION = YES; 325 | CLANG_WARN_INFINITE_RECURSION = YES; 326 | CLANG_WARN_INT_CONVERSION = YES; 327 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 328 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 329 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 330 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 331 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 332 | CLANG_WARN_STRICT_PROTOTYPES = YES; 333 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 334 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 335 | CLANG_WARN_UNREACHABLE_CODE = YES; 336 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 337 | COPY_PHASE_STRIP = NO; 338 | DEBUG_INFORMATION_FORMAT = dwarf; 339 | ENABLE_STRICT_OBJC_MSGSEND = YES; 340 | ENABLE_TESTABILITY = YES; 341 | GCC_C_LANGUAGE_STANDARD = gnu11; 342 | GCC_DYNAMIC_NO_PIC = NO; 343 | GCC_NO_COMMON_BLOCKS = YES; 344 | GCC_OPTIMIZATION_LEVEL = 0; 345 | GCC_PREPROCESSOR_DEFINITIONS = ( 346 | "DEBUG=1", 347 | "$(inherited)", 348 | ); 349 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 350 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 351 | GCC_WARN_UNDECLARED_SELECTOR = YES; 352 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 353 | GCC_WARN_UNUSED_FUNCTION = YES; 354 | GCC_WARN_UNUSED_VARIABLE = YES; 355 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 356 | MTL_FAST_MATH = YES; 357 | ONLY_ACTIVE_ARCH = YES; 358 | SDKROOT = appletvos; 359 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 360 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 361 | TVOS_DEPLOYMENT_TARGET = 13.4; 362 | }; 363 | name = Debug; 364 | }; 365 | 1C53B55A244DD39000B044C0 /* Release */ = { 366 | isa = XCBuildConfiguration; 367 | buildSettings = { 368 | ALWAYS_SEARCH_USER_PATHS = NO; 369 | CLANG_ANALYZER_NONNULL = YES; 370 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 371 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 372 | CLANG_CXX_LIBRARY = "libc++"; 373 | CLANG_ENABLE_MODULES = YES; 374 | CLANG_ENABLE_OBJC_ARC = YES; 375 | CLANG_ENABLE_OBJC_WEAK = YES; 376 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 377 | CLANG_WARN_BOOL_CONVERSION = YES; 378 | CLANG_WARN_COMMA = YES; 379 | CLANG_WARN_CONSTANT_CONVERSION = YES; 380 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 381 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 382 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 383 | CLANG_WARN_EMPTY_BODY = YES; 384 | CLANG_WARN_ENUM_CONVERSION = YES; 385 | CLANG_WARN_INFINITE_RECURSION = YES; 386 | CLANG_WARN_INT_CONVERSION = YES; 387 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 388 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 389 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 390 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 391 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 392 | CLANG_WARN_STRICT_PROTOTYPES = YES; 393 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 394 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 395 | CLANG_WARN_UNREACHABLE_CODE = YES; 396 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 397 | COPY_PHASE_STRIP = NO; 398 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 399 | ENABLE_NS_ASSERTIONS = NO; 400 | ENABLE_STRICT_OBJC_MSGSEND = YES; 401 | GCC_C_LANGUAGE_STANDARD = gnu11; 402 | GCC_NO_COMMON_BLOCKS = YES; 403 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 404 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 405 | GCC_WARN_UNDECLARED_SELECTOR = YES; 406 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 407 | GCC_WARN_UNUSED_FUNCTION = YES; 408 | GCC_WARN_UNUSED_VARIABLE = YES; 409 | MTL_ENABLE_DEBUG_INFO = NO; 410 | MTL_FAST_MATH = YES; 411 | SDKROOT = appletvos; 412 | SWIFT_COMPILATION_MODE = wholemodule; 413 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 414 | TVOS_DEPLOYMENT_TARGET = 13.4; 415 | VALIDATE_PRODUCT = YES; 416 | }; 417 | name = Release; 418 | }; 419 | 1C53B55C244DD39000B044C0 /* Debug */ = { 420 | isa = XCBuildConfiguration; 421 | buildSettings = { 422 | ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; 423 | CODE_SIGN_STYLE = Automatic; 424 | DEVELOPMENT_ASSET_PATHS = "\"TraktTV/Preview Content\""; 425 | DEVELOPMENT_TEAM = 2952RH3AR7; 426 | ENABLE_PREVIEWS = YES; 427 | INFOPLIST_FILE = TraktTV/Info.plist; 428 | LD_RUNPATH_SEARCH_PATHS = ( 429 | "$(inherited)", 430 | "@executable_path/Frameworks", 431 | ); 432 | PRODUCT_BUNDLE_IDENTIFIER = com.aaplab.TraktTV; 433 | PRODUCT_NAME = "$(TARGET_NAME)"; 434 | SWIFT_VERSION = 5.0; 435 | TARGETED_DEVICE_FAMILY = 3; 436 | }; 437 | name = Debug; 438 | }; 439 | 1C53B55D244DD39000B044C0 /* Release */ = { 440 | isa = XCBuildConfiguration; 441 | buildSettings = { 442 | ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; 443 | CODE_SIGN_STYLE = Automatic; 444 | DEVELOPMENT_ASSET_PATHS = "\"TraktTV/Preview Content\""; 445 | DEVELOPMENT_TEAM = 2952RH3AR7; 446 | ENABLE_PREVIEWS = YES; 447 | INFOPLIST_FILE = TraktTV/Info.plist; 448 | LD_RUNPATH_SEARCH_PATHS = ( 449 | "$(inherited)", 450 | "@executable_path/Frameworks", 451 | ); 452 | PRODUCT_BUNDLE_IDENTIFIER = com.aaplab.TraktTV; 453 | PRODUCT_NAME = "$(TARGET_NAME)"; 454 | SWIFT_VERSION = 5.0; 455 | TARGETED_DEVICE_FAMILY = 3; 456 | }; 457 | name = Release; 458 | }; 459 | /* End XCBuildConfiguration section */ 460 | 461 | /* Begin XCConfigurationList section */ 462 | 1C53B544244DD38F00B044C0 /* Build configuration list for PBXProject "TraktTV" */ = { 463 | isa = XCConfigurationList; 464 | buildConfigurations = ( 465 | 1C53B559244DD39000B044C0 /* Debug */, 466 | 1C53B55A244DD39000B044C0 /* Release */, 467 | ); 468 | defaultConfigurationIsVisible = 0; 469 | defaultConfigurationName = Release; 470 | }; 471 | 1C53B55B244DD39000B044C0 /* Build configuration list for PBXNativeTarget "TraktTV" */ = { 472 | isa = XCConfigurationList; 473 | buildConfigurations = ( 474 | 1C53B55C244DD39000B044C0 /* Debug */, 475 | 1C53B55D244DD39000B044C0 /* Release */, 476 | ); 477 | defaultConfigurationIsVisible = 0; 478 | defaultConfigurationName = Release; 479 | }; 480 | /* End XCConfigurationList section */ 481 | 482 | /* Begin XCRemoteSwiftPackageReference section */ 483 | 1C53B586244E0F2E00B044C0 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { 484 | isa = XCRemoteSwiftPackageReference; 485 | repositoryURL = "https://github.com/onevcat/Kingfisher"; 486 | requirement = { 487 | kind = upToNextMajorVersion; 488 | minimumVersion = 5.13.4; 489 | }; 490 | }; 491 | /* End XCRemoteSwiftPackageReference section */ 492 | 493 | /* Begin XCSwiftPackageProductDependency section */ 494 | 1C53B587244E0F2E00B044C0 /* KingfisherSwiftUI */ = { 495 | isa = XCSwiftPackageProductDependency; 496 | package = 1C53B586244E0F2E00B044C0 /* XCRemoteSwiftPackageReference "Kingfisher" */; 497 | productName = KingfisherSwiftUI; 498 | }; 499 | /* End XCSwiftPackageProductDependency section */ 500 | }; 501 | rootObject = 1C53B541244DD38F00B044C0 /* Project object */; 502 | } 503 | -------------------------------------------------------------------------------- /TraktTV.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TraktTV.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TraktTV.xcodeproj/xcuserdata/majid.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | TraktTV.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /TraktTV/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // TraktTV 4 | // 5 | // Created by Majid Jabrayilov on 4/20/20. 6 | // Copyright © 2020 Majid Jabrayilov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | let iso8601 = DateFormatter() 19 | iso8601.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" 20 | 21 | let decoder = JSONDecoder() 22 | decoder.keyDecodingStrategy = .convertFromSnakeCase 23 | decoder.dateDecodingStrategy = .formatted(iso8601) 24 | 25 | let loader = DataLoader(session: .shared, modifiers: [ApiKeyModifier()]) 26 | let searchService = SearchService(loader: loader, decoder: decoder) 27 | let imageService = ImageService(loader: loader, decoder: decoder) 28 | let store = SearchStore(searchService: searchService, imageService: imageService) 29 | // Create the SwiftUI view that provides the window contents. 30 | let contentView = ContentView().environmentObject(store) 31 | 32 | // Use a UIHostingController as window root view controller. 33 | let window = UIWindow(frame: UIScreen.main.bounds) 34 | window.rootViewController = UIHostingController(rootView: contentView) 35 | self.window = window 36 | window.makeKeyAndVisible() 37 | return true 38 | } 39 | 40 | func applicationWillResignActive(_ application: UIApplication) { 41 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 42 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 43 | } 44 | 45 | func applicationDidEnterBackground(_ application: UIApplication) { 46 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 47 | } 48 | 49 | func applicationWillEnterForeground(_ application: UIApplication) { 50 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 51 | } 52 | 53 | func applicationDidBecomeActive(_ application: UIApplication) { 54 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 55 | } 56 | 57 | 58 | } 59 | 60 | -------------------------------------------------------------------------------- /TraktTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /TraktTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TraktTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /TraktTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /TraktTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TraktTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /TraktTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TraktTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /TraktTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TraktTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /TraktTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /TraktTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TraktTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /TraktTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TraktTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "filename" : "App Icon - App Store.imagestack", 5 | "idiom" : "tv", 6 | "role" : "primary-app-icon", 7 | "size" : "1280x768" 8 | }, 9 | { 10 | "filename" : "App Icon.imagestack", 11 | "idiom" : "tv", 12 | "role" : "primary-app-icon", 13 | "size" : "400x240" 14 | }, 15 | { 16 | "filename" : "Top Shelf Image Wide.imageset", 17 | "idiom" : "tv", 18 | "role" : "top-shelf-image-wide", 19 | "size" : "2320x720" 20 | }, 21 | { 22 | "filename" : "Top Shelf Image.imageset", 23 | "idiom" : "tv", 24 | "role" : "top-shelf-image", 25 | "size" : "1920x720" 26 | } 27 | ], 28 | "info" : { 29 | "author" : "xcode", 30 | "version" : 1 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /TraktTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "author" : "xcode", 22 | "version" : 1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /TraktTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "author" : "xcode", 22 | "version" : 1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /TraktTV/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TraktTV/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 | -------------------------------------------------------------------------------- /TraktTV/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // TraktTV 4 | // 5 | // Created by Majid Jabrayilov on 4/20/20. 6 | // Copyright © 2020 Majid Jabrayilov. All rights reserved. 7 | // 8 | import SwiftUI 9 | import KingfisherSwiftUI 10 | 11 | struct ContentView: View { 12 | @EnvironmentObject var store: SearchStore 13 | 14 | var body: some View { 15 | NavigationView { 16 | ScrollView { 17 | VStack(alignment: .leading) { 18 | ForEach(SearchType.allCases, id: \.self) { type in 19 | ShelfView( 20 | title: type.rawValue.capitalized, 21 | shows: self.store.listOfShows(for: type) 22 | ) 23 | } 24 | } 25 | }.onAppear(perform: store.fetch) 26 | } 27 | } 28 | } 29 | 30 | struct ContentView_Previews: PreviewProvider { 31 | static var previews: some View { 32 | ContentView() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /TraktTV/Features/Networking/Extensions/URL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL.swift 3 | // ShowBot 4 | // 5 | // Created by Majid Jabrayilov on 12/19/18. 6 | // Copyright © 2018 aaplab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension URL { 12 | init(staticString string: StaticString) { 13 | guard let url = URL(string: "\(string)") else { 14 | preconditionFailure("Invalid URL string: \(string)") 15 | } 16 | 17 | self = url 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /TraktTV/Features/Networking/Extensions/URLQueryItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLQueryItem.swift 3 | // ShowBot 4 | // 5 | // Created by Majid Jabrayilov on 3/17/19. 6 | // Copyright © 2019 aaplab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension URLQueryItem { 12 | static func limit(value: Int) -> URLQueryItem { 13 | return .init(name: "limit", value: String(value)) 14 | } 15 | 16 | static func extended(value: String = "full") -> URLQueryItem { 17 | return .init(name: "extended", value: value) 18 | } 19 | 20 | static func query(value: String) -> URLQueryItem { 21 | return .init(name: "query", value: value) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /TraktTV/Features/Networking/Logic/DataLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataLoader.swift 3 | // ShowBot 4 | // 5 | // Created by Majid Jabrayilov on 12/19/18. 6 | // Copyright © 2018 aaplab. All rights reserved. 7 | // 8 | import Combine 9 | import Foundation 10 | 11 | final class DataLoader { 12 | private let session: URLSession 13 | private let modifiers: [RequestModifier] 14 | 15 | init(session: URLSession = .shared, modifiers: [RequestModifier] = []) { 16 | self.session = session 17 | self.modifiers = modifiers 18 | } 19 | 20 | func load(_ request: Request) -> AnyPublisher { 21 | let modifiedRequest = modifiers.reduce(request.build()) { $1.modifyRequest($0) } 22 | 23 | return session 24 | .dataTaskPublisher(for: modifiedRequest) 25 | .mapError { $0 } 26 | .map(\.data) 27 | .eraseToAnyPublisher() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /TraktTV/Features/Networking/Models/Endpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint.swift 3 | // ShowBot 4 | // 5 | // Created by Majid Jabrayilov on 12/20/18. 6 | // Copyright © 2018 aaplab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Endpoint: Request { 12 | let path: String 13 | let queryItems: [URLQueryItem] 14 | } 15 | -------------------------------------------------------------------------------- /TraktTV/Features/Networking/Models/Request.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum HTTPMethod: String { 4 | case put = "PUT" 5 | case post = "POST" 6 | case get = "GET" 7 | case delete = "DELETE" 8 | case head = "HEAD" 9 | } 10 | 11 | protocol Request { 12 | var scheme: String { get } 13 | var method: HTTPMethod { get } 14 | var path: String { get } 15 | var host: String { get } 16 | var queryItems: [URLQueryItem] { get } 17 | var headers: [String: String] { get } 18 | var body: Data? { get } 19 | } 20 | 21 | extension Request { 22 | var method: HTTPMethod { return .get } 23 | var scheme: String { return "https" } 24 | var headers: [String: String] { return [:] } 25 | var host: String { return "api.trakt.tv" } 26 | var body: Data? { return nil } 27 | } 28 | 29 | extension Request { 30 | func build() -> URLRequest { 31 | var components = URLComponents() 32 | components.scheme = scheme 33 | components.host = host 34 | components.path = path 35 | components.queryItems = queryItems 36 | 37 | guard let url = components.url else { 38 | preconditionFailure("Invalid url components") 39 | } 40 | 41 | var request = URLRequest(url: url) 42 | request.allHTTPHeaderFields = headers 43 | request.httpMethod = method.rawValue 44 | request.httpBody = body 45 | return request 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /TraktTV/Features/Networking/Models/RequestModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestModifier.swift 3 | // ShowBot 4 | // 5 | // Created by Majid Jabrayilov on 5/26/19. 6 | // Copyright © 2019 aaplab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol RequestModifier { 12 | func modifyRequest(_ request: URLRequest) -> URLRequest 13 | } 14 | 15 | struct ApiKeyModifier: RequestModifier { 16 | private enum Keys { 17 | static let clientId = "1f481b53997fd687e4956e97747af94f41a502460ce18d0bff8c65640072fc57" 18 | } 19 | 20 | func modifyRequest(_ request: URLRequest) -> URLRequest { 21 | var request = request 22 | request.addValue(Keys.clientId, forHTTPHeaderField: "trakt-api-key") 23 | request.addValue("2", forHTTPHeaderField: "trakt-api-version") 24 | request.addValue("application/json", forHTTPHeaderField: "Content-type") 25 | return request 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /TraktTV/Features/Search/Logic/ImageService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageService.swift 3 | // ShowBot 4 | // 5 | // Created by Majid Jabrayilov on 1/3/19. 6 | // Copyright © 2019 aaplab. All rights reserved. 7 | // 8 | import Combine 9 | import Foundation 10 | 11 | final class ImageService { 12 | private let loader: DataLoader 13 | private let decoder: JSONDecoder 14 | 15 | init(loader: DataLoader, decoder: JSONDecoder) { 16 | self.loader = loader 17 | self.decoder = decoder 18 | } 19 | 20 | func fetch(for show: Int) -> AnyPublisher { 21 | return loader 22 | .load(ImagesRequest(show: show)) 23 | .decode(type: TMDBImages.self, decoder: decoder) 24 | .eraseToAnyPublisher() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /TraktTV/Features/Search/Logic/SearchService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchService.swift 3 | // ShowBot 4 | // 5 | // Created by Majid Jabrayilov on 3/17/19. 6 | // Copyright © 2019 aaplab. All rights reserved. 7 | // 8 | import Combine 9 | import Foundation 10 | 11 | final class SearchService { 12 | private let loader: DataLoader 13 | private let decoder: JSONDecoder 14 | 15 | init(loader: DataLoader, decoder: JSONDecoder) { 16 | self.loader = loader 17 | self.decoder = decoder 18 | } 19 | 20 | private struct ShowContainer: Decodable { 21 | let show: Show 22 | } 23 | 24 | func fetchShows(in type: SearchType, limit: Int = 20) -> AnyPublisher<[Show], Error> { 25 | if case .popular = type { 26 | return loader.load(Endpoint.search(in: type, limit: limit)) 27 | .decode(type: [Show].self, decoder: decoder) 28 | .eraseToAnyPublisher() 29 | } else { 30 | return loader.load(Endpoint.search(in: type, limit: limit)) 31 | .decode(type: [ShowContainer].self, decoder: decoder) 32 | .map { $0.map(\.show) } 33 | .eraseToAnyPublisher() 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /TraktTV/Features/Search/Logic/SearchStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchStore.swift 3 | // TraktTV 4 | // 5 | // Created by Majid Jabrayilov on 4/20/20. 6 | // Copyright © 2020 Majid Jabrayilov. All rights reserved. 7 | // 8 | import Foundation 9 | import Combine 10 | 11 | final class SearchStore: ObservableObject { 12 | @Published private(set) var allShows: [Ids: Show] = [:] 13 | @Published private(set) var posters: [Int: URL] = [:] 14 | @Published private(set) var categories: [SearchType: [Ids]] = [:] 15 | 16 | private var cancellables: Set = [] 17 | 18 | private let searchService: SearchService 19 | private let imageService: ImageService 20 | 21 | init(searchService: SearchService, imageService: ImageService) { 22 | self.searchService = searchService 23 | self.imageService = imageService 24 | } 25 | 26 | func fetch() { 27 | fetchShows(for: .popular) 28 | fetchShows(for: .trending) 29 | fetchShows(for: .anticipated) 30 | } 31 | 32 | func listOfShows(for type: SearchType) -> [Show] { 33 | let ids = categories[type] ?? [] 34 | return ids.compactMap { allShows[$0] } 35 | } 36 | 37 | private func fetchShows(for type: SearchType) { 38 | searchService 39 | .fetchShows(in: type) 40 | .receive(on: DispatchQueue.main) 41 | .handleEvents(receiveOutput: { [weak self] shows in 42 | shows.forEach { self?.allShows[$0.ids] = $0 } 43 | self?.categories[type] = shows.map { $0.ids } 44 | }) 45 | .receive(on: DispatchQueue.global()) 46 | .flatMap { 47 | Publishers.Sequence(sequence: $0) 48 | .flatMap { self.imageService.fetch(for: $0.ids.tmdb ?? 0) } 49 | } 50 | .receive(on: DispatchQueue.main) 51 | .sink( 52 | receiveCompletion: { _ in }, 53 | receiveValue: { [weak self] in self?.posters[$0.id] = $0.backdrops.first?.url } 54 | ) 55 | .store(in: &cancellables) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /TraktTV/Features/Search/Model/Endpoint+Images.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagesRequest.swift 3 | // ShowBot 4 | // 5 | // Created by Majid Jabrayilov on 1/3/19. 6 | // Copyright © 2019 aaplab. All rights reserved. 7 | // 8 | import Foundation 9 | 10 | struct ImagesRequest: Request { 11 | let show: Int 12 | 13 | var host: String { "api.themoviedb.org" } 14 | var path: String { return "/3/tv/\(show)/images" } 15 | var queryItems: [URLQueryItem] { 16 | [ 17 | .init(name: "api_key", value: "26032f00317cb09595066add499cb5b4"), 18 | .init(name: "language", value: "en") 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /TraktTV/Features/Search/Model/Endpoint+Search.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint+Search.swift 3 | // ShowBot 4 | // 5 | // Created by Majid Jabrayilov on 12/21/18. 6 | // Copyright © 2018 aaplab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum SearchType: String, CaseIterable { 12 | case popular 13 | case trending 14 | case anticipated 15 | } 16 | 17 | extension Endpoint { 18 | static func search(in type: SearchType, limit: Int = 20) -> Endpoint { 19 | return Endpoint( 20 | path: "/shows/\(type.rawValue)", 21 | queryItems: [.extended(), .limit(value: limit)] 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /TraktTV/Features/Search/Model/Images.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Images.swift 3 | // ShowBot 4 | // 5 | // Created by Majid Jabrayilov on 1/3/19. 6 | // Copyright © 2019 aaplab. All rights reserved. 7 | // 8 | import Foundation 9 | 10 | struct Still: Codable { 11 | let filePath: String 12 | let aspectRatio: Double 13 | } 14 | 15 | extension Still { 16 | var url: URL? { 17 | return URL(string: "https://image.tmdb.org/t/p/original\(filePath)") 18 | } 19 | } 20 | 21 | struct TMDBImages: Codable { 22 | let id: Int 23 | let backdrops: [Still] 24 | let posters: [Still] 25 | } 26 | -------------------------------------------------------------------------------- /TraktTV/Features/Search/Model/Show.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Show.swift 3 | // ShowBot 4 | // 5 | // Created by Majid Jabrayilov on 12/21/18. 6 | // Copyright © 2018 aaplab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Show: Codable, Hashable { 12 | let ids: Ids 13 | let title: String 14 | let year: Int? 15 | let overview: String? 16 | let runtime: Int? 17 | let certification: String? 18 | let network: String? 19 | let country: String? 20 | let trailer: String? 21 | let homepage: String? 22 | let status: String? 23 | let rating: Double? 24 | let votes: Int? 25 | let commentCount: Int? 26 | let updatedAt: Date? 27 | let language: String? 28 | let availableTranslations: [String]? 29 | let genres: [String]? 30 | let airedEpisodes: Int? 31 | } 32 | 33 | struct Ids: Codable, Hashable { 34 | let trakt: Int 35 | let tvdb: Int? 36 | let tmdb: Int? 37 | let slug: String? 38 | let imdb: String? 39 | } 40 | -------------------------------------------------------------------------------- /TraktTV/Features/Search/Views/PosterView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PosterView.swift 3 | // TraktTV 4 | // 5 | // Created by Majid Jabrayilov on 4/21/20. 6 | // Copyright © 2020 Majid Jabrayilov. All rights reserved. 7 | // 8 | import SwiftUI 9 | import KingfisherSwiftUI 10 | 11 | struct PosterView: View { 12 | let image: URL? 13 | let title: String 14 | 15 | @State private var isFocused = false 16 | 17 | var body: some View { 18 | VStack { 19 | image.map { 20 | KFImage($0) 21 | .resizable() 22 | .background(Color.gray) 23 | .frame(width: 400, height: 235, alignment: .center) 24 | } 25 | .cornerRadius(16) 26 | .shadow(radius: 16) 27 | 28 | if isFocused { 29 | Text(title) 30 | } 31 | } 32 | .scaleEffect(isFocused ? 1.05 : 1.0) 33 | .focusable(true) { self.isFocused = $0 } 34 | .animation(.default) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /TraktTV/Features/Search/Views/ShelfView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShelfView.swift 3 | // TraktTV 4 | // 5 | // Created by Majid Jabrayilov on 4/21/20. 6 | // Copyright © 2020 Majid Jabrayilov. All rights reserved. 7 | // 8 | import SwiftUI 9 | 10 | struct ShelfView: View { 11 | @EnvironmentObject var store: SearchStore 12 | 13 | let title: String 14 | let shows: [Show] 15 | 16 | var body: some View { 17 | ScrollView(.horizontal) { 18 | VStack(alignment: .leading) { 19 | Text(title) 20 | .font(.headline) 21 | 22 | HStack { 23 | ForEach(shows, id: \.ids) { show in 24 | PosterView( 25 | image: self.store.posters[show.ids.tmdb ?? 0], 26 | title: show.title 27 | ) 28 | } 29 | } 30 | }.padding() 31 | } 32 | } 33 | } 34 | 35 | struct ShelfView_Previews: PreviewProvider { 36 | static var previews: some View { 37 | let loader = DataLoader(session: .shared, modifiers: []) 38 | let search = SearchService(loader: loader, decoder: .init()) 39 | let image = ImageService(loader: loader, decoder: .init()) 40 | let store = SearchStore(searchService: search, imageService: image) 41 | return ShelfView(title: "Trending", shows: []) 42 | .environmentObject(store) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /TraktTV/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 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | arm64 28 | 29 | UIUserInterfaceStyle 30 | Automatic 31 | NSAppTransportSecurity 32 | 33 | NSAllowsArbitraryLoads 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /TraktTV/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mecid/swiftui-trakttv-app/223fe77b7b3aeb29fe2fa186b7c475308c1f56c6/screenshot.jpg --------------------------------------------------------------------------------