diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index f2c8563b..00000000 --- a/.gitattributes +++ /dev/null @@ -1,3 +0,0 @@ -CHANGELOG.yml merge=union -*.pbxproj merge=union - diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml new file mode 100644 index 00000000..55769ef7 --- /dev/null +++ b/.github/workflows/ios.yml @@ -0,0 +1,32 @@ +name: iOS Build + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + runs-on: macos-latest + steps: + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '16.1.0' + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Tuist + run: | + brew tap tuist/tuist + brew install --formula tuist@4.41.0 + + - name: Tuist Install + run: | + tuist install + + - name: Build iOS App + run: | + tuist build Example --generate --clean --no-binary-cache --platform iOS + \ No newline at end of file diff --git a/.gitignore b/.gitignore index e4f38b95..33faf92f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,77 +1,12 @@ -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - -## Build generated -build/ -DerivedData/ - -## Various settings -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 -xcuserdata/ - -## Other -*.moved-aside -*.xcuserstate -*.DS_Store -vendor/ -.bundle/ -.localio.yml - -## Obj-C/Swift specific -*.hmap -*.ipa -*.dSYM.zip -*.dSYM - -# CocoaPods -# -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# -Pods/ - -# Carthage -# -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts - -Carthage/Build - -# fastlane -# -# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the -# screenshots whenever they are needed. -# For more information about the recommended setup visit: -# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md - -fastlane/report.xml -fastlane/screenshots - -# Code Injection -# -# After new code Injection tools there's a generated folder /iOSInjectionProject -# https://github.com/johnno1962/injectionforxcode - -iOSInjectionProject/ - -# OS X -*.DS_Store +### macOS ### +# General +.DS_Store .AppleDouble .LSOverride -# Icon must end with two \r +# Icon must end with two Icon - # Thumbnails ._* @@ -90,3 +25,46 @@ Icon Network Trash Folder Temporary Items .apdisk + +### Xcode ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno + +### Projects ### +*.xcodeproj +*.xcworkspace + +### Tuist derived files ### +graph.dot +Derived/ + +### Tuist managed dependencies ### +Tuist/.build diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 00000000..f169f597 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,2 @@ +[tools] +tuist = "4.41.0" diff --git a/.package.resolved b/.package.resolved new file mode 100644 index 00000000..280f227a --- /dev/null +++ b/.package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "cb9339b11ed9d7d2ecad83323078648becec11b51488c8702396ac0d9ffb1283", + "pins" : [ + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + } + ], + "version" : 3 +} diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index 0bee604d..00000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -2.3.3 diff --git a/.swift-version b/.swift-version deleted file mode 100644 index 8012ebbb..00000000 --- a/.swift-version +++ /dev/null @@ -1 +0,0 @@ -4.2 \ No newline at end of file diff --git a/Architecture/CoreData.gif b/Architecture/CoreData.gif deleted file mode 100644 index f3c0d751..00000000 Binary files a/Architecture/CoreData.gif and /dev/null differ diff --git a/Architecture/Modules.drawing b/Architecture/Modules.drawing deleted file mode 100644 index ffb90438..00000000 Binary files a/Architecture/Modules.drawing and /dev/null differ diff --git a/Architecture/Modules.png b/Architecture/Modules.png deleted file mode 100644 index 23497e73..00000000 Binary files a/Architecture/Modules.png and /dev/null differ diff --git a/Architecture/ModulesDetails.drawing b/Architecture/ModulesDetails.drawing deleted file mode 100644 index bc37212b..00000000 Binary files a/Architecture/ModulesDetails.drawing and /dev/null differ diff --git a/Architecture/ModulesDetails.png b/Architecture/ModulesDetails.png deleted file mode 100644 index b44d50b7..00000000 Binary files a/Architecture/ModulesDetails.png and /dev/null differ diff --git a/Architecture/Network.gif b/Architecture/Network.gif deleted file mode 100644 index 9fafb59b..00000000 Binary files a/Architecture/Network.gif and /dev/null differ diff --git a/Architecture/Realm.gif b/Architecture/Realm.gif deleted file mode 100644 index 847b4602..00000000 Binary files a/Architecture/Realm.gif and /dev/null differ diff --git a/CleanArchitectureRxSwift.xcodeproj/project.pbxproj b/CleanArchitectureRxSwift.xcodeproj/project.pbxproj deleted file mode 100644 index c0d7535f..00000000 --- a/CleanArchitectureRxSwift.xcodeproj/project.pbxproj +++ /dev/null @@ -1,2672 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 2526A7141E59F8260078870E /* ErrorTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2526A7131E59F8260078870E /* ErrorTracker.swift */; }; - 2526A7161E5A2CD30078870E /* EditPostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2526A7151E5A2CD30078870E /* EditPostViewController.swift */; }; - 256289FC1E91829500A7731C /* UseCaseProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 256289FB1E91829500A7731C /* UseCaseProvider.swift */; }; - 256289FE1E9182A100A7731C /* UseCaseProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 256289FD1E9182A100A7731C /* UseCaseProvider.swift */; }; - 25707C711F23802C00F852F7 /* Encodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25707C701F23802C00F852F7 /* Encodable.swift */; }; - 25707C751F23812E00F852F7 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25707C741F23812E00F852F7 /* Cache.swift */; }; - 25897B041E58BD9100D3563C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897B031E58BD9100D3563C /* AppDelegate.swift */; }; - 25897B091E58BD9100D3563C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 25897B071E58BD9100D3563C /* Main.storyboard */; }; - 25897B0B1E58BD9100D3563C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 25897B0A1E58BD9100D3563C /* Assets.xcassets */; }; - 25897B0E1E58BD9100D3563C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 25897B0C1E58BD9100D3563C /* LaunchScreen.storyboard */; }; - 25897B311E58BF0D00D3563C /* Domain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25897B281E58BF0D00D3563C /* Domain.framework */; }; - 25897B381E58BF0D00D3563C /* DomainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897B371E58BF0D00D3563C /* DomainTests.swift */; }; - 25897B3A1E58BF0D00D3563C /* Domain.h in Headers */ = {isa = PBXBuildFile; fileRef = 25897B2A1E58BF0D00D3563C /* Domain.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25897B3D1E58BF0D00D3563C /* Domain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25897B281E58BF0D00D3563C /* Domain.framework */; }; - 25897B3E1E58BF0D00D3563C /* Domain.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 25897B281E58BF0D00D3563C /* Domain.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 25897B541E58BF3600D3563C /* CoreDataPlatform.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25897B4B1E58BF3600D3563C /* CoreDataPlatform.framework */; }; - 25897B5B1E58BF3600D3563C /* CoreDataPlatformTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897B5A1E58BF3600D3563C /* CoreDataPlatformTests.swift */; }; - 25897B5D1E58BF3600D3563C /* CoreDataPlatform.h in Headers */ = {isa = PBXBuildFile; fileRef = 25897B4D1E58BF3600D3563C /* CoreDataPlatform.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25897B601E58BF3600D3563C /* CoreDataPlatform.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25897B4B1E58BF3600D3563C /* CoreDataPlatform.framework */; }; - 25897B611E58BF3600D3563C /* CoreDataPlatform.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 25897B4B1E58BF3600D3563C /* CoreDataPlatform.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 25897B761E58BF4600D3563C /* RealmPlatform.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25897B6D1E58BF4600D3563C /* RealmPlatform.framework */; }; - 25897B7D1E58BF4600D3563C /* RealmPlatformTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897B7C1E58BF4600D3563C /* RealmPlatformTests.swift */; }; - 25897B7F1E58BF4600D3563C /* RealmPlatform.h in Headers */ = {isa = PBXBuildFile; fileRef = 25897B6F1E58BF4600D3563C /* RealmPlatform.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25897B821E58BF4600D3563C /* RealmPlatform.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25897B6D1E58BF4600D3563C /* RealmPlatform.framework */; }; - 25897B831E58BF4600D3563C /* RealmPlatform.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 25897B6D1E58BF4600D3563C /* RealmPlatform.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 25897B8C1E58C24500D3563C /* CoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897B8B1E58C24500D3563C /* CoreDataStack.swift */; }; - 25897B9C1E58C24B00D3563C /* CDLocation+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897B901E58C24B00D3563C /* CDLocation+Ext.swift */; }; - 25897BA21E58C24B00D3563C /* CDPost+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897B961E58C24B00D3563C /* CDPost+Ext.swift */; }; - 25897BA31E58C24B00D3563C /* DomainConvertibleType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897B981E58C24B00D3563C /* DomainConvertibleType.swift */; }; - 25897BA41E58C24B00D3563C /* Persistable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897B991E58C24B00D3563C /* Persistable.swift */; }; - 25897BA81E58C25000D3563C /* NSManagedObjectContext+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897BA61E58C25000D3563C /* NSManagedObjectContext+Ext.swift */; }; - 25897BA91E58C25000D3563C /* Observable+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897BA71E58C25000D3563C /* Observable+Ext.swift */; }; - 25897BAC1E58C25500D3563C /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 25897BAA1E58C25500D3563C /* Model.xcdatamodeld */; }; - 25897BB71E58C25A00D3563C /* Repository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897BAE1E58C25A00D3563C /* Repository.swift */; }; - 25897BB81E58C25A00D3563C /* ContextScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897BB01E58C25A00D3563C /* ContextScheduler.swift */; }; - 25897BB91E58C25A00D3563C /* FetchedResultsControllerEntityObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897BB11E58C25A00D3563C /* FetchedResultsControllerEntityObserver.swift */; }; - 25897BBA1E58C25A00D3563C /* NSManagedObjectContext+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897BB21E58C25A00D3563C /* NSManagedObjectContext+Rx.swift */; }; - 25897BBB1E58C25A00D3563C /* PostsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897BB41E58C25A00D3563C /* PostsUseCase.swift */; }; - 25897BC61E58C42D00D3563C /* PostsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897BBF1E58C42D00D3563C /* PostsUseCase.swift */; }; - 25897BC91E58C42D00D3563C /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897BC31E58C42D00D3563C /* Location.swift */; }; - 25897BCB1E58C42D00D3563C /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897BC51E58C42D00D3563C /* Post.swift */; }; - 25897BCC1E58C74E00D3563C /* Domain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25897B281E58BF0D00D3563C /* Domain.framework */; }; - 25897BCD1E58C75400D3563C /* Domain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25897B281E58BF0D00D3563C /* Domain.framework */; }; - 25897BF21E58CB8E00D3563C /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897BE21E58CB8E00D3563C /* Application.swift */; }; - 25897BF71E58CB8F00D3563C /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897BE91E58CB8E00D3563C /* ActivityIndicator.swift */; }; - 25897BF91E58CB8F00D3563C /* Observable+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897BEB1E58CB8E00D3563C /* Observable+Ext.swift */; }; - 25897BFB1E58CB8F00D3563C /* Reusable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897BED1E58CB8E00D3563C /* Reusable.swift */; }; - 25897BFC1E58CB8F00D3563C /* ViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25897BEF1E58CB8E00D3563C /* ViewModelType.swift */; }; - 2DEEE5268D97F40515D1C542 /* Pods_DomainTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5764703F1E249BEDFC31014 /* Pods_DomainTests.framework */; }; - 4171B21201FC20FD72A143D0 /* Pods_CoreDataPlatformTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C03758DD874CBFB48A52CE5 /* Pods_CoreDataPlatformTests.framework */; }; - 514BA3A92A644232F50ED1A8 /* Pods_Domain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 771F87FDB28A5E6EC32A9841 /* Pods_Domain.framework */; }; - 515F9083DB52FEDFEE97A5E6 /* PostsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F93CEB3B316E01CDACEA7 /* PostsViewModel.swift */; }; - 515F9256E79E1689380314E5 /* UseCaseProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F987F4A5AF48B9A5451F3 /* UseCaseProvider.swift */; }; - 515F94802804A5FEB69837C8 /* PostsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F9359DD575F7E65FE22AF /* PostsUseCase.swift */; }; - 515F94853EF21BD13D636584 /* PostsNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F94C9D806D051CFBCC327 /* PostsNavigator.swift */; }; - 515F9503C6F079D663CD9072 /* Repository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F92C7826B9C51A1FB4BED /* Repository.swift */; }; - 515F953C72C69919A1104FE7 /* UseCaseProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F9C31AF4C4644DBDAE735 /* UseCaseProvider.swift */; }; - 515F9590146BEE2A0626CFF1 /* PostTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F9AF2348793558FE0CAA7 /* PostTableViewCell.swift */; }; - 515F95A32860E22D7CB9563C /* EditPostViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F9ACD2D474324015B691C /* EditPostViewModel.swift */; }; - 515F95CFED58045AB6B168A4 /* CreatePostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F948FEF12E0E3120DF351 /* CreatePostViewController.swift */; }; - 515F9625B58BCFB77F4AF678 /* CreatePostNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F96DA0B9857BF87D2F2F0 /* CreatePostNavigator.swift */; }; - 515F96794273284723F2848D /* DomainConvertibleType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F9FD5076F041EE00D7E14 /* DomainConvertibleType.swift */; }; - 515F977483234BB090F6D704 /* PostsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F92B305125A2C9E279E71 /* PostsViewController.swift */; }; - 515F978F1DBE7C45646004F1 /* Observable+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F955A785CC73C958268BC /* Observable+Ext.swift */; }; - 515F9850E701F4ECCC669D9B /* CreatePostViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F92E2318F810E8AE29AB7 /* CreatePostViewModel.swift */; }; - 515F9A39814357F9E05282CD /* RunLoopThreadScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F92201894E6338A4BABFA /* RunLoopThreadScheduler.swift */; }; - 515F9B4D6A3BD831B9D4FB8C /* RealmRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F9C13ADD3B42F3CE8A593 /* RealmRepresentable.swift */; }; - 515F9BD6155258BD0C04DF36 /* RMLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F96DF719A053AEF410368 /* RMLocation.swift */; }; - 515F9CD8F2B13D0328B77B6C /* Realm+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F977CB3763872350F7874 /* Realm+Ext.swift */; }; - 515F9DBB950E2ABDB8D7895B /* RMPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F988220373D06226F4EDE /* RMPost.swift */; }; - 515F9EA01C8D63D03B41FF8F /* PostsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515F9DAA48376FC95A9D91B5 /* PostsUseCase.swift */; }; - 59CE6AAC086B5C1AC880DB70 /* Pods_NetworkPlatformTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2AF991A848BB04AAFE03A804 /* Pods_NetworkPlatformTests.framework */; }; - 7752FCB51F716D650079522C /* PostsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7752FCB41F716D650079522C /* PostsViewModelTests.swift */; }; - 7752FCB71F716D7A0079522C /* AllPostsUseCaseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7752FCB61F716D7A0079522C /* AllPostsUseCaseMock.swift */; }; - 7752FCB91F716D940079522C /* PostsNavigatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7752FCB81F716D940079522C /* PostsNavigatorMock.swift */; }; - 7BA4DC961F3AEA380043DAB6 /* PostItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA4DC951F3AEA380043DAB6 /* PostItemViewModel.swift */; }; - 7DFB155E3444551C4DB34AAC /* Pods_CleanArchitectureRxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09A6B74019E724CAD9CA96DC /* Pods_CleanArchitectureRxSwift.framework */; }; - 8A148DC8CA606C8F34807082 /* Pods_NetworkPlatform.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 763D40E220B6FF96E969B284 /* Pods_NetworkPlatform.framework */; }; - 8B0507E0C0AB1064B7372844 /* Pods_RealmPlatform.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 006BDFA0A26FDD0EBA50E777 /* Pods_RealmPlatform.framework */; }; - 9CBC9DB91790C744BC17C099 /* Pods_CleanArchitectureRxSwiftTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 550BE321D44EC009D885BBE1 /* Pods_CleanArchitectureRxSwiftTests.framework */; }; - B60A97CE1FB0C17600009C51 /* EditPostNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B60A97CD1FB0C17600009C51 /* EditPostNavigator.swift */; }; - BCD8C8BC1E73470000F79E3E /* RMAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD8C8BB1E73470000F79E3E /* RMAddress.swift */; }; - BCD8C8BE1E73470C00F79E3E /* RMAlbum.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD8C8BD1E73470C00F79E3E /* RMAlbum.swift */; }; - BCD8C8C01E73471A00F79E3E /* RMComment.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD8C8BF1E73471A00F79E3E /* RMComment.swift */; }; - BCD8C8C21E73472400F79E3E /* RMCompany.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD8C8C11E73472400F79E3E /* RMCompany.swift */; }; - BCD8C8C41E73473300F79E3E /* RMPhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD8C8C31E73473300F79E3E /* RMPhoto.swift */; }; - BCD8C8C61E73474600F79E3E /* RMTodo.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD8C8C51E73474600F79E3E /* RMTodo.swift */; }; - BCD8C8C81E73475000F79E3E /* RMUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD8C8C71E73475000F79E3E /* RMUser.swift */; }; - BD107F521E7298690043D900 /* NetworkPlatform.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BD107F491E7298690043D900 /* NetworkPlatform.framework */; }; - BD107F591E72986A0043D900 /* NetworkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107F581E72986A0043D900 /* NetworkTests.swift */; }; - BD107F5B1E72986A0043D900 /* Network.h in Headers */ = {isa = PBXBuildFile; fileRef = BD107F4B1E7298690043D900 /* Network.h */; settings = {ATTRIBUTES = (Public, ); }; }; - BD107F5E1E72986A0043D900 /* NetworkPlatform.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BD107F491E7298690043D900 /* NetworkPlatform.framework */; }; - BD107F5F1E72986A0043D900 /* NetworkPlatform.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BD107F491E7298690043D900 /* NetworkPlatform.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - BD107F661E72A0D20043D900 /* Domain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25897B281E58BF0D00D3563C /* Domain.framework */; }; - BD107F691E72A0EF0043D900 /* PostsNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107F681E72A0EF0043D900 /* PostsNetwork.swift */; }; - BD107F751E72B1E20043D900 /* Post+Mapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107F741E72B1E20043D900 /* Post+Mapping.swift */; }; - BD107F771E72B71E0043D900 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107F761E72B71E0043D900 /* Comment.swift */; }; - BD107F791E72B7320043D900 /* Album.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107F781E72B7320043D900 /* Album.swift */; }; - BD107F7B1E72B7560043D900 /* Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107F7A1E72B7560043D900 /* Photo.swift */; }; - BD107F7D1E72B7750043D900 /* Todo.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107F7C1E72B7750043D900 /* Todo.swift */; }; - BD107F7F1E72B7820043D900 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107F7E1E72B7820043D900 /* User.swift */; }; - BD107F821E72C5640043D900 /* Address.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107F811E72C5640043D900 /* Address.swift */; }; - BD107F841E72C5740043D900 /* Company.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107F831E72C5740043D900 /* Company.swift */; }; - BD107FA21E72DE230043D900 /* CDAddress+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FA11E72DE230043D900 /* CDAddress+Ext.swift */; }; - BD107FA41E72DE320043D900 /* CDAlbum+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FA31E72DE320043D900 /* CDAlbum+Ext.swift */; }; - BD107FA61E72DE400043D900 /* CDComment+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FA51E72DE400043D900 /* CDComment+Ext.swift */; }; - BD107FA81E72DE4F0043D900 /* CDCompany+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FA71E72DE4F0043D900 /* CDCompany+Ext.swift */; }; - BD107FAA1E72DE5C0043D900 /* CDPhoto+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FA91E72DE5B0043D900 /* CDPhoto+Ext.swift */; }; - BD107FAC1E72DE6B0043D900 /* CDTodo+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FAB1E72DE6B0043D900 /* CDTodo+Ext.swift */; }; - BD107FAE1E72DE760043D900 /* CDUser+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FAD1E72DE760043D900 /* CDUser+Ext.swift */; }; - BD107FC91E730A1D0043D900 /* CDAddress+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FB71E730A1D0043D900 /* CDAddress+CoreDataClass.swift */; }; - BD107FCA1E730A1D0043D900 /* CDAddress+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FB81E730A1D0043D900 /* CDAddress+CoreDataProperties.swift */; }; - BD107FCB1E730A1D0043D900 /* CDAlbum+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FB91E730A1D0043D900 /* CDAlbum+CoreDataClass.swift */; }; - BD107FCC1E730A1D0043D900 /* CDAlbum+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FBA1E730A1D0043D900 /* CDAlbum+CoreDataProperties.swift */; }; - BD107FCD1E730A1D0043D900 /* CDComment+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FBB1E730A1D0043D900 /* CDComment+CoreDataClass.swift */; }; - BD107FCE1E730A1D0043D900 /* CDComment+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FBC1E730A1D0043D900 /* CDComment+CoreDataProperties.swift */; }; - BD107FCF1E730A1D0043D900 /* CDCompany+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FBD1E730A1D0043D900 /* CDCompany+CoreDataClass.swift */; }; - BD107FD01E730A1D0043D900 /* CDCompany+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FBE1E730A1D0043D900 /* CDCompany+CoreDataProperties.swift */; }; - BD107FD11E730A1D0043D900 /* CDLocation+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FBF1E730A1D0043D900 /* CDLocation+CoreDataClass.swift */; }; - BD107FD21E730A1D0043D900 /* CDLocation+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FC01E730A1D0043D900 /* CDLocation+CoreDataProperties.swift */; }; - BD107FD31E730A1D0043D900 /* CDPhoto+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FC11E730A1D0043D900 /* CDPhoto+CoreDataClass.swift */; }; - BD107FD41E730A1D0043D900 /* CDPhoto+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FC21E730A1D0043D900 /* CDPhoto+CoreDataProperties.swift */; }; - BD107FD51E730A1D0043D900 /* CDPost+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FC31E730A1D0043D900 /* CDPost+CoreDataClass.swift */; }; - BD107FD61E730A1D0043D900 /* CDPost+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FC41E730A1D0043D900 /* CDPost+CoreDataProperties.swift */; }; - BD107FD71E730A1D0043D900 /* CDTodo+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FC51E730A1D0043D900 /* CDTodo+CoreDataClass.swift */; }; - BD107FD81E730A1D0043D900 /* CDTodo+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FC61E730A1D0043D900 /* CDTodo+CoreDataProperties.swift */; }; - BD107FD91E730A1D0043D900 /* CDUser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FC71E730A1D0043D900 /* CDUser+CoreDataClass.swift */; }; - BD107FDA1E730A1D0043D900 /* CDUser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD107FC81E730A1D0043D900 /* CDUser+CoreDataProperties.swift */; }; - BD50EEF51E7AD9A300CBEBD4 /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD50EEF41E7AD9A300CBEBD4 /* Network.swift */; }; - BD50EEF71E7ADD5400CBEBD4 /* AlbumsNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD50EEF61E7ADD5400CBEBD4 /* AlbumsNetwork.swift */; }; - BD50EEF91E7ADDA200CBEBD4 /* CommentsNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD50EEF81E7ADDA200CBEBD4 /* CommentsNetwork.swift */; }; - BD50EEFB1E7ADDAF00CBEBD4 /* PhotosNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD50EEFA1E7ADDAF00CBEBD4 /* PhotosNetwork.swift */; }; - BD50EEFD1E7ADDC000CBEBD4 /* TodosNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD50EEFC1E7ADDC000CBEBD4 /* TodosNetwork.swift */; }; - BD50EEFF1E7ADDDD00CBEBD4 /* UsersNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD50EEFE1E7ADDDD00CBEBD4 /* UsersNetwork.swift */; }; - BD50EF011E7AF37C00CBEBD4 /* NetworkProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD50EF001E7AF37C00CBEBD4 /* NetworkProvider.swift */; }; - DE96287CF168CFC64A0DE642 /* Pods_RealmPlatformTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BA0D3602A9ABB65C8AF19365 /* Pods_RealmPlatformTests.framework */; }; - E7CC1C376C9850FDBA8F3DE1 /* Pods_NetworkTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 691DFEEB0D7480A716D51CF9 /* Pods_NetworkTests.framework */; }; - F19EF3D1A8721CEAEA65EE12 /* Pods_CoreDataPlatform.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9C9A3A93769D5899CBDCA89F /* Pods_CoreDataPlatform.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 25897B151E58BD9100D3563C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 25897AF81E58BD9100D3563C /* Project object */; - proxyType = 1; - remoteGlobalIDString = 25897AFF1E58BD9100D3563C; - remoteInfo = CleanArchitectureRxSwift; - }; - 25897B321E58BF0D00D3563C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 25897AF81E58BD9100D3563C /* Project object */; - proxyType = 1; - remoteGlobalIDString = 25897B271E58BF0D00D3563C; - remoteInfo = Domain; - }; - 25897B341E58BF0D00D3563C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 25897AF81E58BD9100D3563C /* Project object */; - proxyType = 1; - remoteGlobalIDString = 25897AFF1E58BD9100D3563C; - remoteInfo = CleanArchitectureRxSwift; - }; - 25897B3B1E58BF0D00D3563C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 25897AF81E58BD9100D3563C /* Project object */; - proxyType = 1; - remoteGlobalIDString = 25897B271E58BF0D00D3563C; - remoteInfo = Domain; - }; - 25897B551E58BF3600D3563C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 25897AF81E58BD9100D3563C /* Project object */; - proxyType = 1; - remoteGlobalIDString = 25897B4A1E58BF3600D3563C; - remoteInfo = CoreDataPlatform; - }; - 25897B571E58BF3600D3563C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 25897AF81E58BD9100D3563C /* Project object */; - proxyType = 1; - remoteGlobalIDString = 25897AFF1E58BD9100D3563C; - remoteInfo = CleanArchitectureRxSwift; - }; - 25897B5E1E58BF3600D3563C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 25897AF81E58BD9100D3563C /* Project object */; - proxyType = 1; - remoteGlobalIDString = 25897B4A1E58BF3600D3563C; - remoteInfo = CoreDataPlatform; - }; - 25897B771E58BF4600D3563C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 25897AF81E58BD9100D3563C /* Project object */; - proxyType = 1; - remoteGlobalIDString = 25897B6C1E58BF4600D3563C; - remoteInfo = RealmPlatform; - }; - 25897B791E58BF4600D3563C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 25897AF81E58BD9100D3563C /* Project object */; - proxyType = 1; - remoteGlobalIDString = 25897AFF1E58BD9100D3563C; - remoteInfo = CleanArchitectureRxSwift; - }; - 25897B801E58BF4600D3563C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 25897AF81E58BD9100D3563C /* Project object */; - proxyType = 1; - remoteGlobalIDString = 25897B6C1E58BF4600D3563C; - remoteInfo = RealmPlatform; - }; - BD107F531E7298690043D900 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 25897AF81E58BD9100D3563C /* Project object */; - proxyType = 1; - remoteGlobalIDString = BD107F481E7298690043D900; - remoteInfo = Network; - }; - BD107F551E72986A0043D900 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 25897AF81E58BD9100D3563C /* Project object */; - proxyType = 1; - remoteGlobalIDString = 25897AFF1E58BD9100D3563C; - remoteInfo = CleanArchitectureRxSwift; - }; - BD107F5C1E72986A0043D900 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 25897AF81E58BD9100D3563C /* Project object */; - proxyType = 1; - remoteGlobalIDString = BD107F481E7298690043D900; - remoteInfo = Network; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 25897B421E58BF0D00D3563C /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 25897B831E58BF4600D3563C /* RealmPlatform.framework in Embed Frameworks */, - 25897B611E58BF3600D3563C /* CoreDataPlatform.framework in Embed Frameworks */, - 25897B3E1E58BF0D00D3563C /* Domain.framework in Embed Frameworks */, - BD107F5F1E72986A0043D900 /* NetworkPlatform.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 006BDFA0A26FDD0EBA50E777 /* Pods_RealmPlatform.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RealmPlatform.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 07BECD54B799E689F7DF0941 /* Pods-Domain.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Domain.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Domain/Pods-Domain.debug.xcconfig"; sourceTree = ""; }; - 081D23F2DCF9DBA930AB0FE7 /* Pods-RealmPlatform.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RealmPlatform.release.xcconfig"; path = "Pods/Target Support Files/Pods-RealmPlatform/Pods-RealmPlatform.release.xcconfig"; sourceTree = ""; }; - 09A6B74019E724CAD9CA96DC /* Pods_CleanArchitectureRxSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CleanArchitectureRxSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 0D4A3B45078CD8ACCC1314EC /* Pods-RealmPlatformTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RealmPlatformTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RealmPlatformTests/Pods-RealmPlatformTests.release.xcconfig"; sourceTree = ""; }; - 2526A7131E59F8260078870E /* ErrorTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorTracker.swift; sourceTree = ""; }; - 2526A7151E5A2CD30078870E /* EditPostViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditPostViewController.swift; sourceTree = ""; }; - 2534B40F1F745F59001997C9 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; - 256289FB1E91829500A7731C /* UseCaseProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UseCaseProvider.swift; sourceTree = ""; }; - 256289FD1E9182A100A7731C /* UseCaseProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UseCaseProvider.swift; sourceTree = ""; }; - 25707C701F23802C00F852F7 /* Encodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Encodable.swift; sourceTree = ""; }; - 25707C741F23812E00F852F7 /* Cache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = ""; }; - 25897B001E58BD9100D3563C /* CleanArchitectureRxSwift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CleanArchitectureRxSwift.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 25897B031E58BD9100D3563C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 25897B081E58BD9100D3563C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 25897B0A1E58BD9100D3563C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 25897B0D1E58BD9100D3563C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 25897B0F1E58BD9100D3563C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 25897B141E58BD9100D3563C /* CleanArchitectureRxSwiftTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CleanArchitectureRxSwiftTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 25897B1A1E58BD9100D3563C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 25897B281E58BF0D00D3563C /* Domain.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Domain.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 25897B2A1E58BF0D00D3563C /* Domain.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Domain.h; sourceTree = ""; }; - 25897B2B1E58BF0D00D3563C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 25897B301E58BF0D00D3563C /* DomainTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DomainTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 25897B371E58BF0D00D3563C /* DomainTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainTests.swift; sourceTree = ""; }; - 25897B391E58BF0D00D3563C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 25897B4B1E58BF3600D3563C /* CoreDataPlatform.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataPlatform.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 25897B4D1E58BF3600D3563C /* CoreDataPlatform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataPlatform.h; sourceTree = ""; }; - 25897B4E1E58BF3600D3563C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 25897B531E58BF3600D3563C /* CoreDataPlatformTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CoreDataPlatformTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 25897B5A1E58BF3600D3563C /* CoreDataPlatformTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataPlatformTests.swift; sourceTree = ""; }; - 25897B5C1E58BF3600D3563C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 25897B6D1E58BF4600D3563C /* RealmPlatform.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RealmPlatform.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 25897B6F1E58BF4600D3563C /* RealmPlatform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RealmPlatform.h; sourceTree = ""; }; - 25897B701E58BF4600D3563C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 25897B751E58BF4600D3563C /* RealmPlatformTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RealmPlatformTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 25897B7C1E58BF4600D3563C /* RealmPlatformTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealmPlatformTests.swift; sourceTree = ""; }; - 25897B7E1E58BF4600D3563C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 25897B8B1E58C24500D3563C /* CoreDataStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataStack.swift; sourceTree = ""; }; - 25897B901E58C24B00D3563C /* CDLocation+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDLocation+Ext.swift"; sourceTree = ""; }; - 25897B961E58C24B00D3563C /* CDPost+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDPost+Ext.swift"; sourceTree = ""; }; - 25897B981E58C24B00D3563C /* DomainConvertibleType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainConvertibleType.swift; sourceTree = ""; }; - 25897B991E58C24B00D3563C /* Persistable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistable.swift; sourceTree = ""; }; - 25897BA61E58C25000D3563C /* NSManagedObjectContext+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Ext.swift"; sourceTree = ""; }; - 25897BA71E58C25000D3563C /* Observable+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Observable+Ext.swift"; sourceTree = ""; }; - 25897BAB1E58C25500D3563C /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; - 25897BAE1E58C25A00D3563C /* Repository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Repository.swift; sourceTree = ""; }; - 25897BB01E58C25A00D3563C /* ContextScheduler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextScheduler.swift; sourceTree = ""; }; - 25897BB11E58C25A00D3563C /* FetchedResultsControllerEntityObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchedResultsControllerEntityObserver.swift; sourceTree = ""; }; - 25897BB21E58C25A00D3563C /* NSManagedObjectContext+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Rx.swift"; sourceTree = ""; }; - 25897BB41E58C25A00D3563C /* PostsUseCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostsUseCase.swift; sourceTree = ""; }; - 25897BBF1E58C42D00D3563C /* PostsUseCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostsUseCase.swift; sourceTree = ""; }; - 25897BC31E58C42D00D3563C /* Location.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = ""; }; - 25897BC51E58C42D00D3563C /* Post.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; - 25897BE21E58CB8E00D3563C /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; - 25897BE91E58CB8E00D3563C /* ActivityIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; - 25897BEB1E58CB8E00D3563C /* Observable+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Observable+Ext.swift"; sourceTree = ""; }; - 25897BED1E58CB8E00D3563C /* Reusable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Reusable.swift; sourceTree = ""; }; - 25897BEF1E58CB8E00D3563C /* ViewModelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModelType.swift; sourceTree = ""; }; - 2AF991A848BB04AAFE03A804 /* Pods_NetworkPlatformTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NetworkPlatformTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 2BBB44DCB902E459F05D68B0 /* Pods-CoreDataPlatformTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoreDataPlatformTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-CoreDataPlatformTests/Pods-CoreDataPlatformTests.release.xcconfig"; sourceTree = ""; }; - 2C03758DD874CBFB48A52CE5 /* Pods_CoreDataPlatformTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CoreDataPlatformTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 3058945350D99DC786F3CDC3 /* Pods-CoreDataPlatform.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoreDataPlatform.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CoreDataPlatform/Pods-CoreDataPlatform.debug.xcconfig"; sourceTree = ""; }; - 3D9EA43A0DD207CAFEADD07C /* Pods-CleanArchitectureRxSwiftTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CleanArchitectureRxSwiftTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CleanArchitectureRxSwiftTests/Pods-CleanArchitectureRxSwiftTests.debug.xcconfig"; sourceTree = ""; }; - 3F5B77FEC1EA2332323A0EB9 /* Pods-CleanArchitectureRxSwift.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CleanArchitectureRxSwift.release.xcconfig"; path = "Pods/Target Support Files/Pods-CleanArchitectureRxSwift/Pods-CleanArchitectureRxSwift.release.xcconfig"; sourceTree = ""; }; - 426939EA03F41A859CEFCA16 /* Pods-NetworkPlatformTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NetworkPlatformTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-NetworkPlatformTests/Pods-NetworkPlatformTests.release.xcconfig"; sourceTree = ""; }; - 4C9D6A2833A962FEF238F391 /* Pods-RealmPlatform.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RealmPlatform.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RealmPlatform/Pods-RealmPlatform.debug.xcconfig"; sourceTree = ""; }; - 4ED44AC018F010F22DC2E0BD /* Pods-NetworkPlatform.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NetworkPlatform.release.xcconfig"; path = "Pods/Target Support Files/Pods-NetworkPlatform/Pods-NetworkPlatform.release.xcconfig"; sourceTree = ""; }; - 515F92201894E6338A4BABFA /* RunLoopThreadScheduler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RunLoopThreadScheduler.swift; sourceTree = ""; }; - 515F92B305125A2C9E279E71 /* PostsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostsViewController.swift; sourceTree = ""; }; - 515F92C7826B9C51A1FB4BED /* Repository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Repository.swift; sourceTree = ""; }; - 515F92E2318F810E8AE29AB7 /* CreatePostViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreatePostViewModel.swift; sourceTree = ""; }; - 515F9359DD575F7E65FE22AF /* PostsUseCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostsUseCase.swift; sourceTree = ""; }; - 515F93CEB3B316E01CDACEA7 /* PostsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostsViewModel.swift; sourceTree = ""; }; - 515F948FEF12E0E3120DF351 /* CreatePostViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreatePostViewController.swift; sourceTree = ""; }; - 515F94C9D806D051CFBCC327 /* PostsNavigator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostsNavigator.swift; sourceTree = ""; }; - 515F955A785CC73C958268BC /* Observable+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Observable+Ext.swift"; sourceTree = ""; }; - 515F96DA0B9857BF87D2F2F0 /* CreatePostNavigator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreatePostNavigator.swift; sourceTree = ""; }; - 515F96DF719A053AEF410368 /* RMLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RMLocation.swift; sourceTree = ""; }; - 515F977CB3763872350F7874 /* Realm+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Realm+Ext.swift"; sourceTree = ""; }; - 515F987F4A5AF48B9A5451F3 /* UseCaseProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UseCaseProvider.swift; sourceTree = ""; }; - 515F988220373D06226F4EDE /* RMPost.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RMPost.swift; sourceTree = ""; }; - 515F9ACD2D474324015B691C /* EditPostViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditPostViewModel.swift; sourceTree = ""; }; - 515F9AF2348793558FE0CAA7 /* PostTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostTableViewCell.swift; sourceTree = ""; }; - 515F9C13ADD3B42F3CE8A593 /* RealmRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmRepresentable.swift; sourceTree = ""; }; - 515F9C31AF4C4644DBDAE735 /* UseCaseProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UseCaseProvider.swift; sourceTree = ""; }; - 515F9DAA48376FC95A9D91B5 /* PostsUseCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostsUseCase.swift; sourceTree = ""; }; - 515F9FD5076F041EE00D7E14 /* DomainConvertibleType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainConvertibleType.swift; sourceTree = ""; }; - 550BE321D44EC009D885BBE1 /* Pods_CleanArchitectureRxSwiftTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CleanArchitectureRxSwiftTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 691DFEEB0D7480A716D51CF9 /* Pods_NetworkTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NetworkTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 6FC0A7F85D212DE861F0D4F5 /* Pods-Network.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Network.release.xcconfig"; path = "Pods/Target Support Files/Pods-Network/Pods-Network.release.xcconfig"; sourceTree = ""; }; - 71C4CC5892A6E3601D801729 /* Pods-CoreDataPlatform.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoreDataPlatform.release.xcconfig"; path = "Pods/Target Support Files/Pods-CoreDataPlatform/Pods-CoreDataPlatform.release.xcconfig"; sourceTree = ""; }; - 763D40E220B6FF96E969B284 /* Pods_NetworkPlatform.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NetworkPlatform.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 771F87FDB28A5E6EC32A9841 /* Pods_Domain.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Domain.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 7752FCB41F716D650079522C /* PostsViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostsViewModelTests.swift; sourceTree = ""; }; - 7752FCB61F716D7A0079522C /* AllPostsUseCaseMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AllPostsUseCaseMock.swift; sourceTree = ""; }; - 7752FCB81F716D940079522C /* PostsNavigatorMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostsNavigatorMock.swift; sourceTree = ""; }; - 7BA4DC951F3AEA380043DAB6 /* PostItemViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostItemViewModel.swift; sourceTree = ""; }; - 81E26E9003A18D1FDD392578 /* Pods-NetworkPlatform.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NetworkPlatform.debug.xcconfig"; path = "Pods/Target Support Files/Pods-NetworkPlatform/Pods-NetworkPlatform.debug.xcconfig"; sourceTree = ""; }; - 84A5797E91E6FA5FA24A4896 /* Pods-CleanArchitectureRxSwift.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CleanArchitectureRxSwift.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CleanArchitectureRxSwift/Pods-CleanArchitectureRxSwift.debug.xcconfig"; sourceTree = ""; }; - 8C5EFC85E3DC2D413D89C8F9 /* Pods_Network.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Network.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 92BD9FF4B0878F787003D01E /* Pods-NetworkTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NetworkTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-NetworkTests/Pods-NetworkTests.release.xcconfig"; sourceTree = ""; }; - 9B6A26BB04BBAABE85CB9EDC /* Pods-DomainTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DomainTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-DomainTests/Pods-DomainTests.release.xcconfig"; sourceTree = ""; }; - 9C9A3A93769D5899CBDCA89F /* Pods_CoreDataPlatform.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CoreDataPlatform.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - A6A063DE6BDC13E3B800BB80 /* Pods-CleanArchitectureRxSwiftTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CleanArchitectureRxSwiftTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-CleanArchitectureRxSwiftTests/Pods-CleanArchitectureRxSwiftTests.release.xcconfig"; sourceTree = ""; }; - A8E1F5AE93A531609690A036 /* Pods-RealmPlatformTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RealmPlatformTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RealmPlatformTests/Pods-RealmPlatformTests.debug.xcconfig"; sourceTree = ""; }; - B0092014AEC057C48B9745EA /* Pods-DomainTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DomainTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-DomainTests/Pods-DomainTests.debug.xcconfig"; sourceTree = ""; }; - B5764703F1E249BEDFC31014 /* Pods_DomainTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DomainTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - B60A97CD1FB0C17600009C51 /* EditPostNavigator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditPostNavigator.swift; sourceTree = ""; }; - BA0D3602A9ABB65C8AF19365 /* Pods_RealmPlatformTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RealmPlatformTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - BC15FAFCFFF7EB7017614B45 /* Pods-NetworkPlatformTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NetworkPlatformTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-NetworkPlatformTests/Pods-NetworkPlatformTests.debug.xcconfig"; sourceTree = ""; }; - BCD8C8BB1E73470000F79E3E /* RMAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RMAddress.swift; sourceTree = ""; }; - BCD8C8BD1E73470C00F79E3E /* RMAlbum.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RMAlbum.swift; sourceTree = ""; }; - BCD8C8BF1E73471A00F79E3E /* RMComment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RMComment.swift; sourceTree = ""; }; - BCD8C8C11E73472400F79E3E /* RMCompany.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RMCompany.swift; sourceTree = ""; }; - BCD8C8C31E73473300F79E3E /* RMPhoto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RMPhoto.swift; sourceTree = ""; }; - BCD8C8C51E73474600F79E3E /* RMTodo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RMTodo.swift; sourceTree = ""; }; - BCD8C8C71E73475000F79E3E /* RMUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RMUser.swift; sourceTree = ""; }; - BD107F491E7298690043D900 /* NetworkPlatform.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NetworkPlatform.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - BD107F4B1E7298690043D900 /* Network.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Network.h; sourceTree = ""; }; - BD107F4C1E7298690043D900 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - BD107F511E7298690043D900 /* NetworkPlatformTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NetworkPlatformTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - BD107F581E72986A0043D900 /* NetworkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkTests.swift; sourceTree = ""; }; - BD107F5A1E72986A0043D900 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - BD107F681E72A0EF0043D900 /* PostsNetwork.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostsNetwork.swift; sourceTree = ""; }; - BD107F741E72B1E20043D900 /* Post+Mapping.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Post+Mapping.swift"; sourceTree = ""; }; - BD107F761E72B71E0043D900 /* Comment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; - BD107F781E72B7320043D900 /* Album.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Album.swift; sourceTree = ""; }; - BD107F7A1E72B7560043D900 /* Photo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Photo.swift; sourceTree = ""; }; - BD107F7C1E72B7750043D900 /* Todo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Todo.swift; sourceTree = ""; }; - BD107F7E1E72B7820043D900 /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; - BD107F811E72C5640043D900 /* Address.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Address.swift; sourceTree = ""; }; - BD107F831E72C5740043D900 /* Company.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Company.swift; sourceTree = ""; }; - BD107FA11E72DE230043D900 /* CDAddress+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDAddress+Ext.swift"; sourceTree = ""; }; - BD107FA31E72DE320043D900 /* CDAlbum+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDAlbum+Ext.swift"; sourceTree = ""; }; - BD107FA51E72DE400043D900 /* CDComment+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDComment+Ext.swift"; sourceTree = ""; }; - BD107FA71E72DE4F0043D900 /* CDCompany+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDCompany+Ext.swift"; sourceTree = ""; }; - BD107FA91E72DE5B0043D900 /* CDPhoto+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDPhoto+Ext.swift"; sourceTree = ""; }; - BD107FAB1E72DE6B0043D900 /* CDTodo+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDTodo+Ext.swift"; sourceTree = ""; }; - BD107FAD1E72DE760043D900 /* CDUser+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDUser+Ext.swift"; sourceTree = ""; }; - BD107FB71E730A1D0043D900 /* CDAddress+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDAddress+CoreDataClass.swift"; sourceTree = ""; }; - BD107FB81E730A1D0043D900 /* CDAddress+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDAddress+CoreDataProperties.swift"; sourceTree = ""; }; - BD107FB91E730A1D0043D900 /* CDAlbum+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDAlbum+CoreDataClass.swift"; sourceTree = ""; }; - BD107FBA1E730A1D0043D900 /* CDAlbum+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDAlbum+CoreDataProperties.swift"; sourceTree = ""; }; - BD107FBB1E730A1D0043D900 /* CDComment+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDComment+CoreDataClass.swift"; sourceTree = ""; }; - BD107FBC1E730A1D0043D900 /* CDComment+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDComment+CoreDataProperties.swift"; sourceTree = ""; }; - BD107FBD1E730A1D0043D900 /* CDCompany+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDCompany+CoreDataClass.swift"; sourceTree = ""; }; - BD107FBE1E730A1D0043D900 /* CDCompany+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDCompany+CoreDataProperties.swift"; sourceTree = ""; }; - BD107FBF1E730A1D0043D900 /* CDLocation+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDLocation+CoreDataClass.swift"; sourceTree = ""; }; - BD107FC01E730A1D0043D900 /* CDLocation+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDLocation+CoreDataProperties.swift"; sourceTree = ""; }; - BD107FC11E730A1D0043D900 /* CDPhoto+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDPhoto+CoreDataClass.swift"; sourceTree = ""; }; - BD107FC21E730A1D0043D900 /* CDPhoto+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDPhoto+CoreDataProperties.swift"; sourceTree = ""; }; - BD107FC31E730A1D0043D900 /* CDPost+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDPost+CoreDataClass.swift"; sourceTree = ""; }; - BD107FC41E730A1D0043D900 /* CDPost+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDPost+CoreDataProperties.swift"; sourceTree = ""; }; - BD107FC51E730A1D0043D900 /* CDTodo+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDTodo+CoreDataClass.swift"; sourceTree = ""; }; - BD107FC61E730A1D0043D900 /* CDTodo+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDTodo+CoreDataProperties.swift"; sourceTree = ""; }; - BD107FC71E730A1D0043D900 /* CDUser+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDUser+CoreDataClass.swift"; sourceTree = ""; }; - BD107FC81E730A1D0043D900 /* CDUser+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CDUser+CoreDataProperties.swift"; sourceTree = ""; }; - BD50EEF41E7AD9A300CBEBD4 /* Network.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; - BD50EEF61E7ADD5400CBEBD4 /* AlbumsNetwork.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumsNetwork.swift; sourceTree = ""; }; - BD50EEF81E7ADDA200CBEBD4 /* CommentsNetwork.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommentsNetwork.swift; sourceTree = ""; }; - BD50EEFA1E7ADDAF00CBEBD4 /* PhotosNetwork.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotosNetwork.swift; sourceTree = ""; }; - BD50EEFC1E7ADDC000CBEBD4 /* TodosNetwork.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TodosNetwork.swift; sourceTree = ""; }; - BD50EEFE1E7ADDDD00CBEBD4 /* UsersNetwork.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsersNetwork.swift; sourceTree = ""; }; - BD50EF001E7AF37C00CBEBD4 /* NetworkProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProvider.swift; sourceTree = ""; }; - CF6E48F9E3D9085D854008BE /* Pods-Network.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Network.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Network/Pods-Network.debug.xcconfig"; sourceTree = ""; }; - D28BD18435DE56EF95FB0E9D /* Pods-NetworkTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NetworkTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-NetworkTests/Pods-NetworkTests.debug.xcconfig"; sourceTree = ""; }; - F1F89859FF9197F25DB915C7 /* Pods-CoreDataPlatformTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoreDataPlatformTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CoreDataPlatformTests/Pods-CoreDataPlatformTests.debug.xcconfig"; sourceTree = ""; }; - FA1AB793DF561BEEC8C591F7 /* Pods-Domain.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Domain.release.xcconfig"; path = "Pods/Target Support Files/Pods-Domain/Pods-Domain.release.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 25897AFD1E58BD9100D3563C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 25897B821E58BF4600D3563C /* RealmPlatform.framework in Frameworks */, - 25897B601E58BF3600D3563C /* CoreDataPlatform.framework in Frameworks */, - 25897B3D1E58BF0D00D3563C /* Domain.framework in Frameworks */, - 7DFB155E3444551C4DB34AAC /* Pods_CleanArchitectureRxSwift.framework in Frameworks */, - BD107F5E1E72986A0043D900 /* NetworkPlatform.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B111E58BD9100D3563C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9CBC9DB91790C744BC17C099 /* Pods_CleanArchitectureRxSwiftTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B241E58BF0D00D3563C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 514BA3A92A644232F50ED1A8 /* Pods_Domain.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B2D1E58BF0D00D3563C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 25897B311E58BF0D00D3563C /* Domain.framework in Frameworks */, - 2DEEE5268D97F40515D1C542 /* Pods_DomainTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B471E58BF3600D3563C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 25897BCC1E58C74E00D3563C /* Domain.framework in Frameworks */, - F19EF3D1A8721CEAEA65EE12 /* Pods_CoreDataPlatform.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B501E58BF3600D3563C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 25897B541E58BF3600D3563C /* CoreDataPlatform.framework in Frameworks */, - 4171B21201FC20FD72A143D0 /* Pods_CoreDataPlatformTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B691E58BF4600D3563C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 25897BCD1E58C75400D3563C /* Domain.framework in Frameworks */, - 8B0507E0C0AB1064B7372844 /* Pods_RealmPlatform.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B721E58BF4600D3563C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 25897B761E58BF4600D3563C /* RealmPlatform.framework in Frameworks */, - DE96287CF168CFC64A0DE642 /* Pods_RealmPlatformTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - BD107F451E7298690043D900 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - BD107F661E72A0D20043D900 /* Domain.framework in Frameworks */, - 8A148DC8CA606C8F34807082 /* Pods_NetworkPlatform.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - BD107F4E1E7298690043D900 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - BD107F521E7298690043D900 /* NetworkPlatform.framework in Frameworks */, - E7CC1C376C9850FDBA8F3DE1 /* Pods_NetworkTests.framework in Frameworks */, - 59CE6AAC086B5C1AC880DB70 /* Pods_NetworkPlatformTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 23C0DA07B3F46EB59E072F7E /* Frameworks */ = { - isa = PBXGroup; - children = ( - 09A6B74019E724CAD9CA96DC /* Pods_CleanArchitectureRxSwift.framework */, - 550BE321D44EC009D885BBE1 /* Pods_CleanArchitectureRxSwiftTests.framework */, - 9C9A3A93769D5899CBDCA89F /* Pods_CoreDataPlatform.framework */, - 2C03758DD874CBFB48A52CE5 /* Pods_CoreDataPlatformTests.framework */, - 771F87FDB28A5E6EC32A9841 /* Pods_Domain.framework */, - B5764703F1E249BEDFC31014 /* Pods_DomainTests.framework */, - 006BDFA0A26FDD0EBA50E777 /* Pods_RealmPlatform.framework */, - BA0D3602A9ABB65C8AF19365 /* Pods_RealmPlatformTests.framework */, - 8C5EFC85E3DC2D413D89C8F9 /* Pods_Network.framework */, - 691DFEEB0D7480A716D51CF9 /* Pods_NetworkTests.framework */, - 763D40E220B6FF96E969B284 /* Pods_NetworkPlatform.framework */, - 2AF991A848BB04AAFE03A804 /* Pods_NetworkPlatformTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 25707C6F1F237F7700F852F7 /* Encodable */ = { - isa = PBXGroup; - children = ( - 25707C701F23802C00F852F7 /* Encodable.swift */, - ); - path = Encodable; - sourceTree = ""; - }; - 25707C731F2380E500F852F7 /* Cache */ = { - isa = PBXGroup; - children = ( - 25707C741F23812E00F852F7 /* Cache.swift */, - ); - path = Cache; - sourceTree = ""; - }; - 25897AF71E58BD9100D3563C = { - isa = PBXGroup; - children = ( - 2534B40F1F745F59001997C9 /* README.md */, - 25897B021E58BD9100D3563C /* CleanArchitectureRxSwift */, - 25897B171E58BD9100D3563C /* CleanArchitectureRxSwiftTests */, - 25897B4C1E58BF3600D3563C /* CoreDataPlatform */, - 25897B591E58BF3600D3563C /* CoreDataPlatformTests */, - 25897B291E58BF0D00D3563C /* Domain */, - 25897B361E58BF0D00D3563C /* DomainTests */, - 23C0DA07B3F46EB59E072F7E /* Frameworks */, - BD107F4A1E7298690043D900 /* NetworkPlatform */, - BD107F571E72986A0043D900 /* NetworkPlatformTests */, - A3C0A06A8E4F121C929B96B4 /* Pods */, - 25897B011E58BD9100D3563C /* Products */, - 25897B6E1E58BF4600D3563C /* RealmPlatform */, - 25897B7B1E58BF4600D3563C /* RealmPlatformTests */, - ); - sourceTree = ""; - }; - 25897B011E58BD9100D3563C /* Products */ = { - isa = PBXGroup; - children = ( - 25897B001E58BD9100D3563C /* CleanArchitectureRxSwift.app */, - 25897B141E58BD9100D3563C /* CleanArchitectureRxSwiftTests.xctest */, - 25897B281E58BF0D00D3563C /* Domain.framework */, - 25897B301E58BF0D00D3563C /* DomainTests.xctest */, - 25897B4B1E58BF3600D3563C /* CoreDataPlatform.framework */, - 25897B531E58BF3600D3563C /* CoreDataPlatformTests.xctest */, - 25897B6D1E58BF4600D3563C /* RealmPlatform.framework */, - 25897B751E58BF4600D3563C /* RealmPlatformTests.xctest */, - BD107F491E7298690043D900 /* NetworkPlatform.framework */, - BD107F511E7298690043D900 /* NetworkPlatformTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 25897B021E58BD9100D3563C /* CleanArchitectureRxSwift */ = { - isa = PBXGroup; - children = ( - 25897BE11E58CB8E00D3563C /* Application */, - 25897BEE1E58CB8E00D3563C /* Common */, - 515F903025B664C6A43D7962 /* Scenes */, - 25897BE81E58CB8E00D3563C /* Utility */, - 25897B031E58BD9100D3563C /* AppDelegate.swift */, - 25897B0A1E58BD9100D3563C /* Assets.xcassets */, - 25897B0F1E58BD9100D3563C /* Info.plist */, - 25897B0C1E58BD9100D3563C /* LaunchScreen.storyboard */, - 25897B071E58BD9100D3563C /* Main.storyboard */, - ); - path = CleanArchitectureRxSwift; - sourceTree = ""; - }; - 25897B171E58BD9100D3563C /* CleanArchitectureRxSwiftTests */ = { - isa = PBXGroup; - children = ( - 7752FCB21F716D410079522C /* Scenes */, - 25897B1A1E58BD9100D3563C /* Info.plist */, - ); - path = CleanArchitectureRxSwiftTests; - sourceTree = ""; - }; - 25897B291E58BF0D00D3563C /* Domain */ = { - isa = PBXGroup; - children = ( - 25897BC21E58C42D00D3563C /* Entries */, - 25897BBE1E58C42D00D3563C /* UseCases */, - 25897B2A1E58BF0D00D3563C /* Domain.h */, - 25897B2B1E58BF0D00D3563C /* Info.plist */, - ); - path = Domain; - sourceTree = ""; - }; - 25897B361E58BF0D00D3563C /* DomainTests */ = { - isa = PBXGroup; - children = ( - 25897B371E58BF0D00D3563C /* DomainTests.swift */, - 25897B391E58BF0D00D3563C /* Info.plist */, - ); - path = DomainTests; - sourceTree = ""; - }; - 25897B4C1E58BF3600D3563C /* CoreDataPlatform */ = { - isa = PBXGroup; - children = ( - 25897B8A1E58C24500D3563C /* CoreDataStack */, - 25897B8D1E58C24B00D3563C /* Entities */, - 25897BA51E58C25000D3563C /* Extensions */, - 25897BAD1E58C25A00D3563C /* Repository */, - 25897BAF1E58C25A00D3563C /* RxCoreData */, - 25897BB31E58C25A00D3563C /* UseCases */, - 25897B4D1E58BF3600D3563C /* CoreDataPlatform.h */, - 25897B4E1E58BF3600D3563C /* Info.plist */, - 25897BAA1E58C25500D3563C /* Model.xcdatamodeld */, - ); - path = CoreDataPlatform; - sourceTree = ""; - }; - 25897B591E58BF3600D3563C /* CoreDataPlatformTests */ = { - isa = PBXGroup; - children = ( - 25897B5A1E58BF3600D3563C /* CoreDataPlatformTests.swift */, - 25897B5C1E58BF3600D3563C /* Info.plist */, - ); - path = CoreDataPlatformTests; - sourceTree = ""; - }; - 25897B6E1E58BF4600D3563C /* RealmPlatform */ = { - isa = PBXGroup; - children = ( - 515F921AB2989DB17DD621B6 /* Entities */, - 515F988DB4ECDA46B682145C /* Repository */, - 515F94D09679E59029EB0F24 /* UseCases */, - 515F96C0E02DA60D7AC0B94B /* Utility */, - 25897B701E58BF4600D3563C /* Info.plist */, - 25897B6F1E58BF4600D3563C /* RealmPlatform.h */, - ); - path = RealmPlatform; - sourceTree = ""; - }; - 25897B7B1E58BF4600D3563C /* RealmPlatformTests */ = { - isa = PBXGroup; - children = ( - 25897B7E1E58BF4600D3563C /* Info.plist */, - 25897B7C1E58BF4600D3563C /* RealmPlatformTests.swift */, - ); - path = RealmPlatformTests; - sourceTree = ""; - }; - 25897B8A1E58C24500D3563C /* CoreDataStack */ = { - isa = PBXGroup; - children = ( - 25897B8B1E58C24500D3563C /* CoreDataStack.swift */, - ); - path = CoreDataStack; - sourceTree = ""; - }; - 25897B8D1E58C24B00D3563C /* Entities */ = { - isa = PBXGroup; - children = ( - 25897B971E58C24B00D3563C /* Convertion */, - BD107FB71E730A1D0043D900 /* CDAddress+CoreDataClass.swift */, - BD107FB81E730A1D0043D900 /* CDAddress+CoreDataProperties.swift */, - BD107FA11E72DE230043D900 /* CDAddress+Ext.swift */, - BD107FB91E730A1D0043D900 /* CDAlbum+CoreDataClass.swift */, - BD107FBA1E730A1D0043D900 /* CDAlbum+CoreDataProperties.swift */, - BD107FA31E72DE320043D900 /* CDAlbum+Ext.swift */, - BD107FBB1E730A1D0043D900 /* CDComment+CoreDataClass.swift */, - BD107FBC1E730A1D0043D900 /* CDComment+CoreDataProperties.swift */, - BD107FA51E72DE400043D900 /* CDComment+Ext.swift */, - BD107FBD1E730A1D0043D900 /* CDCompany+CoreDataClass.swift */, - BD107FBE1E730A1D0043D900 /* CDCompany+CoreDataProperties.swift */, - BD107FA71E72DE4F0043D900 /* CDCompany+Ext.swift */, - BD107FBF1E730A1D0043D900 /* CDLocation+CoreDataClass.swift */, - BD107FC01E730A1D0043D900 /* CDLocation+CoreDataProperties.swift */, - 25897B901E58C24B00D3563C /* CDLocation+Ext.swift */, - BD107FC11E730A1D0043D900 /* CDPhoto+CoreDataClass.swift */, - BD107FC21E730A1D0043D900 /* CDPhoto+CoreDataProperties.swift */, - BD107FA91E72DE5B0043D900 /* CDPhoto+Ext.swift */, - BD107FC31E730A1D0043D900 /* CDPost+CoreDataClass.swift */, - BD107FC41E730A1D0043D900 /* CDPost+CoreDataProperties.swift */, - 25897B961E58C24B00D3563C /* CDPost+Ext.swift */, - BD107FC51E730A1D0043D900 /* CDTodo+CoreDataClass.swift */, - BD107FC61E730A1D0043D900 /* CDTodo+CoreDataProperties.swift */, - BD107FAB1E72DE6B0043D900 /* CDTodo+Ext.swift */, - BD107FC71E730A1D0043D900 /* CDUser+CoreDataClass.swift */, - BD107FC81E730A1D0043D900 /* CDUser+CoreDataProperties.swift */, - BD107FAD1E72DE760043D900 /* CDUser+Ext.swift */, - ); - path = Entities; - sourceTree = ""; - }; - 25897B971E58C24B00D3563C /* Convertion */ = { - isa = PBXGroup; - children = ( - 25897B981E58C24B00D3563C /* DomainConvertibleType.swift */, - 25897B991E58C24B00D3563C /* Persistable.swift */, - ); - path = Convertion; - sourceTree = ""; - }; - 25897BA51E58C25000D3563C /* Extensions */ = { - isa = PBXGroup; - children = ( - 25897BA61E58C25000D3563C /* NSManagedObjectContext+Ext.swift */, - 25897BA71E58C25000D3563C /* Observable+Ext.swift */, - ); - path = Extensions; - sourceTree = ""; - }; - 25897BAD1E58C25A00D3563C /* Repository */ = { - isa = PBXGroup; - children = ( - 25897BAE1E58C25A00D3563C /* Repository.swift */, - ); - path = Repository; - sourceTree = ""; - }; - 25897BAF1E58C25A00D3563C /* RxCoreData */ = { - isa = PBXGroup; - children = ( - 25897BB01E58C25A00D3563C /* ContextScheduler.swift */, - 25897BB11E58C25A00D3563C /* FetchedResultsControllerEntityObserver.swift */, - 25897BB21E58C25A00D3563C /* NSManagedObjectContext+Rx.swift */, - ); - path = RxCoreData; - sourceTree = ""; - }; - 25897BB31E58C25A00D3563C /* UseCases */ = { - isa = PBXGroup; - children = ( - 25897BB41E58C25A00D3563C /* PostsUseCase.swift */, - 256289FB1E91829500A7731C /* UseCaseProvider.swift */, - ); - path = UseCases; - sourceTree = ""; - }; - 25897BBE1E58C42D00D3563C /* UseCases */ = { - isa = PBXGroup; - children = ( - 25897BBF1E58C42D00D3563C /* PostsUseCase.swift */, - 515F9C31AF4C4644DBDAE735 /* UseCaseProvider.swift */, - ); - path = UseCases; - sourceTree = ""; - }; - 25897BC21E58C42D00D3563C /* Entries */ = { - isa = PBXGroup; - children = ( - BD107F811E72C5640043D900 /* Address.swift */, - BD107F781E72B7320043D900 /* Album.swift */, - BD107F761E72B71E0043D900 /* Comment.swift */, - BD107F831E72C5740043D900 /* Company.swift */, - 25897BC31E58C42D00D3563C /* Location.swift */, - BD107F7A1E72B7560043D900 /* Photo.swift */, - 25897BC51E58C42D00D3563C /* Post.swift */, - BD107F7C1E72B7750043D900 /* Todo.swift */, - BD107F7E1E72B7820043D900 /* User.swift */, - ); - path = Entries; - sourceTree = ""; - }; - 25897BE11E58CB8E00D3563C /* Application */ = { - isa = PBXGroup; - children = ( - 25897BE21E58CB8E00D3563C /* Application.swift */, - ); - path = Application; - sourceTree = ""; - }; - 25897BE81E58CB8E00D3563C /* Utility */ = { - isa = PBXGroup; - children = ( - 25897BE91E58CB8E00D3563C /* ActivityIndicator.swift */, - 2526A7131E59F8260078870E /* ErrorTracker.swift */, - 25897BEB1E58CB8E00D3563C /* Observable+Ext.swift */, - 25897BED1E58CB8E00D3563C /* Reusable.swift */, - ); - path = Utility; - sourceTree = ""; - }; - 25897BEE1E58CB8E00D3563C /* Common */ = { - isa = PBXGroup; - children = ( - 25897BEF1E58CB8E00D3563C /* ViewModelType.swift */, - ); - path = Common; - sourceTree = ""; - }; - 515F903025B664C6A43D7962 /* Scenes */ = { - isa = PBXGroup; - children = ( - 515F94479B9C0EF5329F9428 /* AllPosts */, - 515F9B161E41C7D130E180EF /* CreatePost */, - 515F9401ED57113EEF898228 /* EditPost */, - ); - path = Scenes; - sourceTree = ""; - }; - 515F921AB2989DB17DD621B6 /* Entities */ = { - isa = PBXGroup; - children = ( - 515F93C28B4316E9B2F00D42 /* Convertion */, - BCD8C8BB1E73470000F79E3E /* RMAddress.swift */, - BCD8C8BD1E73470C00F79E3E /* RMAlbum.swift */, - BCD8C8BF1E73471A00F79E3E /* RMComment.swift */, - BCD8C8C11E73472400F79E3E /* RMCompany.swift */, - 515F96DF719A053AEF410368 /* RMLocation.swift */, - BCD8C8C31E73473300F79E3E /* RMPhoto.swift */, - 515F988220373D06226F4EDE /* RMPost.swift */, - BCD8C8C51E73474600F79E3E /* RMTodo.swift */, - BCD8C8C71E73475000F79E3E /* RMUser.swift */, - ); - path = Entities; - sourceTree = ""; - }; - 515F93C28B4316E9B2F00D42 /* Convertion */ = { - isa = PBXGroup; - children = ( - 515F9FD5076F041EE00D7E14 /* DomainConvertibleType.swift */, - 515F9C13ADD3B42F3CE8A593 /* RealmRepresentable.swift */, - ); - path = Convertion; - sourceTree = ""; - }; - 515F9401ED57113EEF898228 /* EditPost */ = { - isa = PBXGroup; - children = ( - B60A97CD1FB0C17600009C51 /* EditPostNavigator.swift */, - 2526A7151E5A2CD30078870E /* EditPostViewController.swift */, - 515F9ACD2D474324015B691C /* EditPostViewModel.swift */, - ); - path = EditPost; - sourceTree = ""; - }; - 515F94479B9C0EF5329F9428 /* AllPosts */ = { - isa = PBXGroup; - children = ( - 515F9AF2348793558FE0CAA7 /* PostTableViewCell.swift */, - 515F94C9D806D051CFBCC327 /* PostsNavigator.swift */, - 515F92B305125A2C9E279E71 /* PostsViewController.swift */, - 515F93CEB3B316E01CDACEA7 /* PostsViewModel.swift */, - 7BA4DC951F3AEA380043DAB6 /* PostItemViewModel.swift */, - ); - path = AllPosts; - sourceTree = ""; - }; - 515F94BB8B79F597BACF0614 /* UseCases */ = { - isa = PBXGroup; - children = ( - 515F9DAA48376FC95A9D91B5 /* PostsUseCase.swift */, - 515F987F4A5AF48B9A5451F3 /* UseCaseProvider.swift */, - ); - path = UseCases; - sourceTree = ""; - }; - 515F94D09679E59029EB0F24 /* UseCases */ = { - isa = PBXGroup; - children = ( - 515F9359DD575F7E65FE22AF /* PostsUseCase.swift */, - 256289FD1E9182A100A7731C /* UseCaseProvider.swift */, - ); - path = UseCases; - sourceTree = ""; - }; - 515F96C0E02DA60D7AC0B94B /* Utility */ = { - isa = PBXGroup; - children = ( - 515F9F0B54EB33278403FD4D /* Extensions */, - 515F9A9E07211156210A87C6 /* RxUnits */, - ); - path = Utility; - sourceTree = ""; - }; - 515F988DB4ECDA46B682145C /* Repository */ = { - isa = PBXGroup; - children = ( - 515F92C7826B9C51A1FB4BED /* Repository.swift */, - ); - path = Repository; - sourceTree = ""; - }; - 515F9A9E07211156210A87C6 /* RxUnits */ = { - isa = PBXGroup; - children = ( - 515F92201894E6338A4BABFA /* RunLoopThreadScheduler.swift */, - ); - path = RxUnits; - sourceTree = ""; - }; - 515F9B161E41C7D130E180EF /* CreatePost */ = { - isa = PBXGroup; - children = ( - 515F96DA0B9857BF87D2F2F0 /* CreatePostNavigator.swift */, - 515F948FEF12E0E3120DF351 /* CreatePostViewController.swift */, - 515F92E2318F810E8AE29AB7 /* CreatePostViewModel.swift */, - ); - path = CreatePost; - sourceTree = ""; - }; - 515F9F0B54EB33278403FD4D /* Extensions */ = { - isa = PBXGroup; - children = ( - 515F955A785CC73C958268BC /* Observable+Ext.swift */, - 515F977CB3763872350F7874 /* Realm+Ext.swift */, - ); - path = Extensions; - sourceTree = ""; - }; - 7752FCB21F716D410079522C /* Scenes */ = { - isa = PBXGroup; - children = ( - 7752FCB31F716D4A0079522C /* AllPosts */, - ); - path = Scenes; - sourceTree = ""; - }; - 7752FCB31F716D4A0079522C /* AllPosts */ = { - isa = PBXGroup; - children = ( - 7752FCB41F716D650079522C /* PostsViewModelTests.swift */, - 7752FCB61F716D7A0079522C /* AllPostsUseCaseMock.swift */, - 7752FCB81F716D940079522C /* PostsNavigatorMock.swift */, - ); - path = AllPosts; - sourceTree = ""; - }; - A3C0A06A8E4F121C929B96B4 /* Pods */ = { - isa = PBXGroup; - children = ( - 84A5797E91E6FA5FA24A4896 /* Pods-CleanArchitectureRxSwift.debug.xcconfig */, - 3F5B77FEC1EA2332323A0EB9 /* Pods-CleanArchitectureRxSwift.release.xcconfig */, - 3D9EA43A0DD207CAFEADD07C /* Pods-CleanArchitectureRxSwiftTests.debug.xcconfig */, - A6A063DE6BDC13E3B800BB80 /* Pods-CleanArchitectureRxSwiftTests.release.xcconfig */, - 3058945350D99DC786F3CDC3 /* Pods-CoreDataPlatform.debug.xcconfig */, - 71C4CC5892A6E3601D801729 /* Pods-CoreDataPlatform.release.xcconfig */, - F1F89859FF9197F25DB915C7 /* Pods-CoreDataPlatformTests.debug.xcconfig */, - 2BBB44DCB902E459F05D68B0 /* Pods-CoreDataPlatformTests.release.xcconfig */, - 07BECD54B799E689F7DF0941 /* Pods-Domain.debug.xcconfig */, - FA1AB793DF561BEEC8C591F7 /* Pods-Domain.release.xcconfig */, - B0092014AEC057C48B9745EA /* Pods-DomainTests.debug.xcconfig */, - 9B6A26BB04BBAABE85CB9EDC /* Pods-DomainTests.release.xcconfig */, - 4C9D6A2833A962FEF238F391 /* Pods-RealmPlatform.debug.xcconfig */, - 081D23F2DCF9DBA930AB0FE7 /* Pods-RealmPlatform.release.xcconfig */, - A8E1F5AE93A531609690A036 /* Pods-RealmPlatformTests.debug.xcconfig */, - 0D4A3B45078CD8ACCC1314EC /* Pods-RealmPlatformTests.release.xcconfig */, - CF6E48F9E3D9085D854008BE /* Pods-Network.debug.xcconfig */, - 6FC0A7F85D212DE861F0D4F5 /* Pods-Network.release.xcconfig */, - D28BD18435DE56EF95FB0E9D /* Pods-NetworkTests.debug.xcconfig */, - 92BD9FF4B0878F787003D01E /* Pods-NetworkTests.release.xcconfig */, - 81E26E9003A18D1FDD392578 /* Pods-NetworkPlatform.debug.xcconfig */, - 4ED44AC018F010F22DC2E0BD /* Pods-NetworkPlatform.release.xcconfig */, - BC15FAFCFFF7EB7017614B45 /* Pods-NetworkPlatformTests.debug.xcconfig */, - 426939EA03F41A859CEFCA16 /* Pods-NetworkPlatformTests.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - BD107F4A1E7298690043D900 /* NetworkPlatform */ = { - isa = PBXGroup; - children = ( - 25707C731F2380E500F852F7 /* Cache */, - BD107F671E72A0DC0043D900 /* API */, - BD107F731E72B1790043D900 /* Entries */, - BD50EEF31E7AD99400CBEBD4 /* Network */, - 515F94BB8B79F597BACF0614 /* UseCases */, - BD107F4C1E7298690043D900 /* Info.plist */, - BD107F4B1E7298690043D900 /* Network.h */, - ); - path = NetworkPlatform; - sourceTree = ""; - }; - BD107F571E72986A0043D900 /* NetworkPlatformTests */ = { - isa = PBXGroup; - children = ( - BD107F5A1E72986A0043D900 /* Info.plist */, - BD107F581E72986A0043D900 /* NetworkTests.swift */, - ); - path = NetworkPlatformTests; - sourceTree = ""; - }; - BD107F671E72A0DC0043D900 /* API */ = { - isa = PBXGroup; - children = ( - BD50EEF61E7ADD5400CBEBD4 /* AlbumsNetwork.swift */, - BD50EEF81E7ADDA200CBEBD4 /* CommentsNetwork.swift */, - BD50EEFA1E7ADDAF00CBEBD4 /* PhotosNetwork.swift */, - BD107F681E72A0EF0043D900 /* PostsNetwork.swift */, - BD50EEFC1E7ADDC000CBEBD4 /* TodosNetwork.swift */, - BD50EEFE1E7ADDDD00CBEBD4 /* UsersNetwork.swift */, - ); - path = API; - sourceTree = ""; - }; - BD107F731E72B1790043D900 /* Entries */ = { - isa = PBXGroup; - children = ( - 25707C6F1F237F7700F852F7 /* Encodable */, - BD107F741E72B1E20043D900 /* Post+Mapping.swift */, - ); - path = Entries; - sourceTree = ""; - }; - BD50EEF31E7AD99400CBEBD4 /* Network */ = { - isa = PBXGroup; - children = ( - BD50EEF41E7AD9A300CBEBD4 /* Network.swift */, - BD50EF001E7AF37C00CBEBD4 /* NetworkProvider.swift */, - ); - path = Network; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXHeadersBuildPhase section */ - 25897B251E58BF0D00D3563C /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - 25897B3A1E58BF0D00D3563C /* Domain.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B481E58BF3600D3563C /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - 25897B5D1E58BF3600D3563C /* CoreDataPlatform.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B6A1E58BF4600D3563C /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - 25897B7F1E58BF4600D3563C /* RealmPlatform.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - BD107F461E7298690043D900 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - BD107F5B1E72986A0043D900 /* Network.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - -/* Begin PBXNativeTarget section */ - 25897AFF1E58BD9100D3563C /* CleanArchitectureRxSwift */ = { - isa = PBXNativeTarget; - buildConfigurationList = 25897B1D1E58BD9100D3563C /* Build configuration list for PBXNativeTarget "CleanArchitectureRxSwift" */; - buildPhases = ( - 4D40437EE66B816D743E97B5 /* [CP] Check Pods Manifest.lock */, - 25897AFC1E58BD9100D3563C /* Sources */, - 25897AFD1E58BD9100D3563C /* Frameworks */, - 25897AFE1E58BD9100D3563C /* Resources */, - 25897B421E58BF0D00D3563C /* Embed Frameworks */, - 17B07EE7C6E1F9585169BCEF /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 25897B3C1E58BF0D00D3563C /* PBXTargetDependency */, - 25897B5F1E58BF3600D3563C /* PBXTargetDependency */, - 25897B811E58BF4600D3563C /* PBXTargetDependency */, - BD107F5D1E72986A0043D900 /* PBXTargetDependency */, - ); - name = CleanArchitectureRxSwift; - productName = CleanArchitectureRxSwift; - productReference = 25897B001E58BD9100D3563C /* CleanArchitectureRxSwift.app */; - productType = "com.apple.product-type.application"; - }; - 25897B131E58BD9100D3563C /* CleanArchitectureRxSwiftTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 25897B201E58BD9100D3563C /* Build configuration list for PBXNativeTarget "CleanArchitectureRxSwiftTests" */; - buildPhases = ( - 015131BB381CBD181905CBC2 /* [CP] Check Pods Manifest.lock */, - 25897B101E58BD9100D3563C /* Sources */, - 25897B111E58BD9100D3563C /* Frameworks */, - 25897B121E58BD9100D3563C /* Resources */, - 99D05AF51EAFBA63AB33FF1A /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 25897B161E58BD9100D3563C /* PBXTargetDependency */, - ); - name = CleanArchitectureRxSwiftTests; - productName = CleanArchitectureRxSwiftTests; - productReference = 25897B141E58BD9100D3563C /* CleanArchitectureRxSwiftTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 25897B271E58BF0D00D3563C /* Domain */ = { - isa = PBXNativeTarget; - buildConfigurationList = 25897B3F1E58BF0D00D3563C /* Build configuration list for PBXNativeTarget "Domain" */; - buildPhases = ( - 9DDCC75CAD78374C078CD054 /* [CP] Check Pods Manifest.lock */, - 25897B231E58BF0D00D3563C /* Sources */, - 25897B241E58BF0D00D3563C /* Frameworks */, - 25897B251E58BF0D00D3563C /* Headers */, - 25897B261E58BF0D00D3563C /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Domain; - productName = Domain; - productReference = 25897B281E58BF0D00D3563C /* Domain.framework */; - productType = "com.apple.product-type.framework"; - }; - 25897B2F1E58BF0D00D3563C /* DomainTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 25897B431E58BF0D00D3563C /* Build configuration list for PBXNativeTarget "DomainTests" */; - buildPhases = ( - D0775CFF21514545A5358821 /* [CP] Check Pods Manifest.lock */, - 25897B2C1E58BF0D00D3563C /* Sources */, - 25897B2D1E58BF0D00D3563C /* Frameworks */, - 25897B2E1E58BF0D00D3563C /* Resources */, - 203BDCA1039B7C197B857E89 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 25897B331E58BF0D00D3563C /* PBXTargetDependency */, - 25897B351E58BF0D00D3563C /* PBXTargetDependency */, - ); - name = DomainTests; - productName = DomainTests; - productReference = 25897B301E58BF0D00D3563C /* DomainTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 25897B4A1E58BF3600D3563C /* CoreDataPlatform */ = { - isa = PBXNativeTarget; - buildConfigurationList = 25897B621E58BF3600D3563C /* Build configuration list for PBXNativeTarget "CoreDataPlatform" */; - buildPhases = ( - 5E2552E63A43C10038965F18 /* [CP] Check Pods Manifest.lock */, - 25897B461E58BF3600D3563C /* Sources */, - 25897B471E58BF3600D3563C /* Frameworks */, - 25897B481E58BF3600D3563C /* Headers */, - 25897B491E58BF3600D3563C /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = CoreDataPlatform; - productName = CoreDataPlatform; - productReference = 25897B4B1E58BF3600D3563C /* CoreDataPlatform.framework */; - productType = "com.apple.product-type.framework"; - }; - 25897B521E58BF3600D3563C /* CoreDataPlatformTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 25897B651E58BF3600D3563C /* Build configuration list for PBXNativeTarget "CoreDataPlatformTests" */; - buildPhases = ( - F6F2EEDD7AAC48A1BC11CD4B /* [CP] Check Pods Manifest.lock */, - 25897B4F1E58BF3600D3563C /* Sources */, - 25897B501E58BF3600D3563C /* Frameworks */, - 25897B511E58BF3600D3563C /* Resources */, - 7E130B3F8A11A4D1B94AA921 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 25897B561E58BF3600D3563C /* PBXTargetDependency */, - 25897B581E58BF3600D3563C /* PBXTargetDependency */, - ); - name = CoreDataPlatformTests; - productName = CoreDataPlatformTests; - productReference = 25897B531E58BF3600D3563C /* CoreDataPlatformTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 25897B6C1E58BF4600D3563C /* RealmPlatform */ = { - isa = PBXNativeTarget; - buildConfigurationList = 25897B841E58BF4600D3563C /* Build configuration list for PBXNativeTarget "RealmPlatform" */; - buildPhases = ( - 2D5A32A95B40355B4FA7628D /* [CP] Check Pods Manifest.lock */, - 25897B681E58BF4600D3563C /* Sources */, - 25897B691E58BF4600D3563C /* Frameworks */, - 25897B6A1E58BF4600D3563C /* Headers */, - 25897B6B1E58BF4600D3563C /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = RealmPlatform; - productName = RealmPlatform; - productReference = 25897B6D1E58BF4600D3563C /* RealmPlatform.framework */; - productType = "com.apple.product-type.framework"; - }; - 25897B741E58BF4600D3563C /* RealmPlatformTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 25897B871E58BF4600D3563C /* Build configuration list for PBXNativeTarget "RealmPlatformTests" */; - buildPhases = ( - B5E1898DADCA47342737CBEF /* [CP] Check Pods Manifest.lock */, - 25897B711E58BF4600D3563C /* Sources */, - 25897B721E58BF4600D3563C /* Frameworks */, - 25897B731E58BF4600D3563C /* Resources */, - BCF36B73A0784820E1EB47E1 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 25897B781E58BF4600D3563C /* PBXTargetDependency */, - 25897B7A1E58BF4600D3563C /* PBXTargetDependency */, - ); - name = RealmPlatformTests; - productName = RealmPlatformTests; - productReference = 25897B751E58BF4600D3563C /* RealmPlatformTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - BD107F481E7298690043D900 /* NetworkPlatform */ = { - isa = PBXNativeTarget; - buildConfigurationList = BD107F641E72986A0043D900 /* Build configuration list for PBXNativeTarget "NetworkPlatform" */; - buildPhases = ( - EAF10E0C4D76CD8157380408 /* [CP] Check Pods Manifest.lock */, - BD107F441E7298690043D900 /* Sources */, - BD107F451E7298690043D900 /* Frameworks */, - BD107F461E7298690043D900 /* Headers */, - BD107F471E7298690043D900 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = NetworkPlatform; - productName = Network; - productReference = BD107F491E7298690043D900 /* NetworkPlatform.framework */; - productType = "com.apple.product-type.framework"; - }; - BD107F501E7298690043D900 /* NetworkPlatformTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = BD107F651E72986A0043D900 /* Build configuration list for PBXNativeTarget "NetworkPlatformTests" */; - buildPhases = ( - 1E57CD4E628D91D6EA26728B /* [CP] Check Pods Manifest.lock */, - BD107F4D1E7298690043D900 /* Sources */, - BD107F4E1E7298690043D900 /* Frameworks */, - BD107F4F1E7298690043D900 /* Resources */, - 4A499AE12136E7CA9FB2E37C /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - BD107F541E7298690043D900 /* PBXTargetDependency */, - BD107F561E72986A0043D900 /* PBXTargetDependency */, - ); - name = NetworkPlatformTests; - productName = NetworkTests; - productReference = BD107F511E7298690043D900 /* NetworkPlatformTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 25897AF81E58BD9100D3563C /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0820; - LastUpgradeCheck = 1000; - ORGANIZATIONNAME = sergdort; - TargetAttributes = { - 25897AFF1E58BD9100D3563C = { - CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = BU4T5Z97JD; - ProvisioningStyle = Automatic; - }; - 25897B131E58BD9100D3563C = { - CreatedOnToolsVersion = 8.2.1; - LastSwiftMigration = 0830; - ProvisioningStyle = Automatic; - TestTargetID = 25897AFF1E58BD9100D3563C; - }; - 25897B271E58BF0D00D3563C = { - CreatedOnToolsVersion = 8.2.1; - ProvisioningStyle = Automatic; - }; - 25897B2F1E58BF0D00D3563C = { - CreatedOnToolsVersion = 8.2.1; - ProvisioningStyle = Automatic; - TestTargetID = 25897AFF1E58BD9100D3563C; - }; - 25897B4A1E58BF3600D3563C = { - CreatedOnToolsVersion = 8.2.1; - ProvisioningStyle = Automatic; - }; - 25897B521E58BF3600D3563C = { - CreatedOnToolsVersion = 8.2.1; - ProvisioningStyle = Automatic; - TestTargetID = 25897AFF1E58BD9100D3563C; - }; - 25897B6C1E58BF4600D3563C = { - CreatedOnToolsVersion = 8.2.1; - ProvisioningStyle = Automatic; - }; - 25897B741E58BF4600D3563C = { - CreatedOnToolsVersion = 8.2.1; - ProvisioningStyle = Automatic; - TestTargetID = 25897AFF1E58BD9100D3563C; - }; - BD107F481E7298690043D900 = { - CreatedOnToolsVersion = 8.2.1; - LastSwiftMigration = 0820; - ProvisioningStyle = Automatic; - }; - BD107F501E7298690043D900 = { - CreatedOnToolsVersion = 8.2.1; - ProvisioningStyle = Automatic; - TestTargetID = 25897AFF1E58BD9100D3563C; - }; - }; - }; - buildConfigurationList = 25897AFB1E58BD9100D3563C /* Build configuration list for PBXProject "CleanArchitectureRxSwift" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 25897AF71E58BD9100D3563C; - productRefGroup = 25897B011E58BD9100D3563C /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 25897AFF1E58BD9100D3563C /* CleanArchitectureRxSwift */, - 25897B131E58BD9100D3563C /* CleanArchitectureRxSwiftTests */, - 25897B271E58BF0D00D3563C /* Domain */, - 25897B2F1E58BF0D00D3563C /* DomainTests */, - 25897B4A1E58BF3600D3563C /* CoreDataPlatform */, - 25897B521E58BF3600D3563C /* CoreDataPlatformTests */, - 25897B6C1E58BF4600D3563C /* RealmPlatform */, - 25897B741E58BF4600D3563C /* RealmPlatformTests */, - BD107F481E7298690043D900 /* NetworkPlatform */, - BD107F501E7298690043D900 /* NetworkPlatformTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 25897AFE1E58BD9100D3563C /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 25897B0E1E58BD9100D3563C /* LaunchScreen.storyboard in Resources */, - 25897B0B1E58BD9100D3563C /* Assets.xcassets in Resources */, - 25897B091E58BD9100D3563C /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B121E58BD9100D3563C /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B261E58BF0D00D3563C /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B2E1E58BF0D00D3563C /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B491E58BF3600D3563C /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B511E58BF3600D3563C /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B6B1E58BF4600D3563C /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B731E58BF4600D3563C /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - BD107F471E7298690043D900 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - BD107F4F1E7298690043D900 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 015131BB381CBD181905CBC2 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-CleanArchitectureRxSwiftTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 17B07EE7C6E1F9585169BCEF /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-CleanArchitectureRxSwift/Pods-CleanArchitectureRxSwift-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/QueryKit/QueryKit.framework", - "${BUILT_PRODUCTS_DIR}/RxAtomic/RxAtomic.framework", - "${BUILT_PRODUCTS_DIR}/RxCocoa/RxCocoa.framework", - "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework", - "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", - "${BUILT_PRODUCTS_DIR}/RxAlamofire/RxAlamofire.framework", - "${BUILT_PRODUCTS_DIR}/Realm/Realm.framework", - "${BUILT_PRODUCTS_DIR}/RealmSwift/RealmSwift.framework", - "${BUILT_PRODUCTS_DIR}/RxRealm/RxRealm.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/QueryKit.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxAtomic.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxCocoa.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxSwift.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxAlamofire.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RealmSwift.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxRealm.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-CleanArchitectureRxSwift/Pods-CleanArchitectureRxSwift-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 1E57CD4E628D91D6EA26728B /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-NetworkPlatformTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 203BDCA1039B7C197B857E89 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-DomainTests/Pods-DomainTests-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/RxAtomic/RxAtomic.framework", - "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework", - "${BUILT_PRODUCTS_DIR}/Nimble/Nimble.framework", - "${BUILT_PRODUCTS_DIR}/RxBlocking/RxBlocking.framework", - "${BUILT_PRODUCTS_DIR}/RxTest/RxTest.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxAtomic.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxSwift.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Nimble.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxBlocking.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxTest.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-DomainTests/Pods-DomainTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 2D5A32A95B40355B4FA7628D /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RealmPlatform-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 4A499AE12136E7CA9FB2E37C /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-NetworkPlatformTests/Pods-NetworkPlatformTests-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/RxAtomic/RxAtomic.framework", - "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework", - "${BUILT_PRODUCTS_DIR}/Nimble/Nimble.framework", - "${BUILT_PRODUCTS_DIR}/RxBlocking/RxBlocking.framework", - "${BUILT_PRODUCTS_DIR}/RxTest/RxTest.framework", - "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", - "${BUILT_PRODUCTS_DIR}/RxAlamofire/RxAlamofire.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxAtomic.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxSwift.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Nimble.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxBlocking.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxTest.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxAlamofire.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-NetworkPlatformTests/Pods-NetworkPlatformTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 4D40437EE66B816D743E97B5 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-CleanArchitectureRxSwift-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 5E2552E63A43C10038965F18 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-CoreDataPlatform-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 7E130B3F8A11A4D1B94AA921 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-CoreDataPlatformTests/Pods-CoreDataPlatformTests-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/RxAtomic/RxAtomic.framework", - "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework", - "${BUILT_PRODUCTS_DIR}/Nimble/Nimble.framework", - "${BUILT_PRODUCTS_DIR}/RxBlocking/RxBlocking.framework", - "${BUILT_PRODUCTS_DIR}/RxTest/RxTest.framework", - "${BUILT_PRODUCTS_DIR}/QueryKit/QueryKit.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxAtomic.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxSwift.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Nimble.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxBlocking.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxTest.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/QueryKit.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-CoreDataPlatformTests/Pods-CoreDataPlatformTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 99D05AF51EAFBA63AB33FF1A /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-CleanArchitectureRxSwiftTests/Pods-CleanArchitectureRxSwiftTests-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/RxAtomic/RxAtomic.framework", - "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework", - "${BUILT_PRODUCTS_DIR}/Nimble/Nimble.framework", - "${BUILT_PRODUCTS_DIR}/RxBlocking/RxBlocking.framework", - "${BUILT_PRODUCTS_DIR}/RxTest/RxTest.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxAtomic.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxSwift.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Nimble.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxBlocking.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxTest.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-CleanArchitectureRxSwiftTests/Pods-CleanArchitectureRxSwiftTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 9DDCC75CAD78374C078CD054 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Domain-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - B5E1898DADCA47342737CBEF /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RealmPlatformTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - BCF36B73A0784820E1EB47E1 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-RealmPlatformTests/Pods-RealmPlatformTests-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/RxAtomic/RxAtomic.framework", - "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework", - "${BUILT_PRODUCTS_DIR}/Nimble/Nimble.framework", - "${BUILT_PRODUCTS_DIR}/RxBlocking/RxBlocking.framework", - "${BUILT_PRODUCTS_DIR}/RxTest/RxTest.framework", - "${BUILT_PRODUCTS_DIR}/QueryKit/QueryKit.framework", - "${BUILT_PRODUCTS_DIR}/Realm/Realm.framework", - "${BUILT_PRODUCTS_DIR}/RealmSwift/RealmSwift.framework", - "${BUILT_PRODUCTS_DIR}/RxRealm/RxRealm.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxAtomic.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxSwift.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Nimble.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxBlocking.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxTest.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/QueryKit.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RealmSwift.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxRealm.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-RealmPlatformTests/Pods-RealmPlatformTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - D0775CFF21514545A5358821 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-DomainTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - EAF10E0C4D76CD8157380408 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-NetworkPlatform-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - F6F2EEDD7AAC48A1BC11CD4B /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-CoreDataPlatformTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 25897AFC1E58BD9100D3563C /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 25897BFB1E58CB8F00D3563C /* Reusable.swift in Sources */, - 25897BF21E58CB8E00D3563C /* Application.swift in Sources */, - 25897BF91E58CB8F00D3563C /* Observable+Ext.swift in Sources */, - 25897BF71E58CB8F00D3563C /* ActivityIndicator.swift in Sources */, - 25897B041E58BD9100D3563C /* AppDelegate.swift in Sources */, - 2526A7141E59F8260078870E /* ErrorTracker.swift in Sources */, - 25897BFC1E58CB8F00D3563C /* ViewModelType.swift in Sources */, - 515F94853EF21BD13D636584 /* PostsNavigator.swift in Sources */, - 515F977483234BB090F6D704 /* PostsViewController.swift in Sources */, - 515F9083DB52FEDFEE97A5E6 /* PostsViewModel.swift in Sources */, - 515F9590146BEE2A0626CFF1 /* PostTableViewCell.swift in Sources */, - B60A97CE1FB0C17600009C51 /* EditPostNavigator.swift in Sources */, - 515F95CFED58045AB6B168A4 /* CreatePostViewController.swift in Sources */, - 7BA4DC961F3AEA380043DAB6 /* PostItemViewModel.swift in Sources */, - 515F9625B58BCFB77F4AF678 /* CreatePostNavigator.swift in Sources */, - 2526A7161E5A2CD30078870E /* EditPostViewController.swift in Sources */, - 515F9850E701F4ECCC669D9B /* CreatePostViewModel.swift in Sources */, - 515F95A32860E22D7CB9563C /* EditPostViewModel.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B101E58BD9100D3563C /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 7752FCB51F716D650079522C /* PostsViewModelTests.swift in Sources */, - 7752FCB91F716D940079522C /* PostsNavigatorMock.swift in Sources */, - 7752FCB71F716D7A0079522C /* AllPostsUseCaseMock.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B231E58BF0D00D3563C /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 25897BC61E58C42D00D3563C /* PostsUseCase.swift in Sources */, - 25897BC91E58C42D00D3563C /* Location.swift in Sources */, - BD107F7B1E72B7560043D900 /* Photo.swift in Sources */, - BD107F771E72B71E0043D900 /* Comment.swift in Sources */, - 25897BCB1E58C42D00D3563C /* Post.swift in Sources */, - BD107F821E72C5640043D900 /* Address.swift in Sources */, - BD107F791E72B7320043D900 /* Album.swift in Sources */, - BD107F7D1E72B7750043D900 /* Todo.swift in Sources */, - BD107F841E72C5740043D900 /* Company.swift in Sources */, - BD107F7F1E72B7820043D900 /* User.swift in Sources */, - 515F953C72C69919A1104FE7 /* UseCaseProvider.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B2C1E58BF0D00D3563C /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 25897B381E58BF0D00D3563C /* DomainTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B461E58BF3600D3563C /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 25897BBA1E58C25A00D3563C /* NSManagedObjectContext+Rx.swift in Sources */, - BD107FD21E730A1D0043D900 /* CDLocation+CoreDataProperties.swift in Sources */, - BD107FD71E730A1D0043D900 /* CDTodo+CoreDataClass.swift in Sources */, - 25897BB81E58C25A00D3563C /* ContextScheduler.swift in Sources */, - 25897B8C1E58C24500D3563C /* CoreDataStack.swift in Sources */, - 25897BAC1E58C25500D3563C /* Model.xcdatamodeld in Sources */, - BD107FD01E730A1D0043D900 /* CDCompany+CoreDataProperties.swift in Sources */, - BD107FD51E730A1D0043D900 /* CDPost+CoreDataClass.swift in Sources */, - 25897BA21E58C24B00D3563C /* CDPost+Ext.swift in Sources */, - BD107FD11E730A1D0043D900 /* CDLocation+CoreDataClass.swift in Sources */, - BD107FD41E730A1D0043D900 /* CDPhoto+CoreDataProperties.swift in Sources */, - BD107FDA1E730A1D0043D900 /* CDUser+CoreDataProperties.swift in Sources */, - BD107FAC1E72DE6B0043D900 /* CDTodo+Ext.swift in Sources */, - BD107FD81E730A1D0043D900 /* CDTodo+CoreDataProperties.swift in Sources */, - BD107FA41E72DE320043D900 /* CDAlbum+Ext.swift in Sources */, - BD107FA61E72DE400043D900 /* CDComment+Ext.swift in Sources */, - 25897BA81E58C25000D3563C /* NSManagedObjectContext+Ext.swift in Sources */, - BD107FD61E730A1D0043D900 /* CDPost+CoreDataProperties.swift in Sources */, - BD107FCA1E730A1D0043D900 /* CDAddress+CoreDataProperties.swift in Sources */, - 25897BA41E58C24B00D3563C /* Persistable.swift in Sources */, - BD107FAE1E72DE760043D900 /* CDUser+Ext.swift in Sources */, - 25897B9C1E58C24B00D3563C /* CDLocation+Ext.swift in Sources */, - 25897BB71E58C25A00D3563C /* Repository.swift in Sources */, - BD107FAA1E72DE5C0043D900 /* CDPhoto+Ext.swift in Sources */, - BD107FCF1E730A1D0043D900 /* CDCompany+CoreDataClass.swift in Sources */, - BD107FD31E730A1D0043D900 /* CDPhoto+CoreDataClass.swift in Sources */, - 256289FC1E91829500A7731C /* UseCaseProvider.swift in Sources */, - 25897BB91E58C25A00D3563C /* FetchedResultsControllerEntityObserver.swift in Sources */, - BD107FCB1E730A1D0043D900 /* CDAlbum+CoreDataClass.swift in Sources */, - BD107FCE1E730A1D0043D900 /* CDComment+CoreDataProperties.swift in Sources */, - BD107FCD1E730A1D0043D900 /* CDComment+CoreDataClass.swift in Sources */, - BD107FA21E72DE230043D900 /* CDAddress+Ext.swift in Sources */, - BD107FA81E72DE4F0043D900 /* CDCompany+Ext.swift in Sources */, - BD107FD91E730A1D0043D900 /* CDUser+CoreDataClass.swift in Sources */, - 25897BA91E58C25000D3563C /* Observable+Ext.swift in Sources */, - 25897BA31E58C24B00D3563C /* DomainConvertibleType.swift in Sources */, - BD107FC91E730A1D0043D900 /* CDAddress+CoreDataClass.swift in Sources */, - BD107FCC1E730A1D0043D900 /* CDAlbum+CoreDataProperties.swift in Sources */, - 25897BBB1E58C25A00D3563C /* PostsUseCase.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B4F1E58BF3600D3563C /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 25897B5B1E58BF3600D3563C /* CoreDataPlatformTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B681E58BF4600D3563C /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 515F9B4D6A3BD831B9D4FB8C /* RealmRepresentable.swift in Sources */, - BCD8C8C61E73474600F79E3E /* RMTodo.swift in Sources */, - BCD8C8C41E73473300F79E3E /* RMPhoto.swift in Sources */, - 515F96794273284723F2848D /* DomainConvertibleType.swift in Sources */, - 515F9DBB950E2ABDB8D7895B /* RMPost.swift in Sources */, - 256289FE1E9182A100A7731C /* UseCaseProvider.swift in Sources */, - 515F9BD6155258BD0C04DF36 /* RMLocation.swift in Sources */, - 515F9CD8F2B13D0328B77B6C /* Realm+Ext.swift in Sources */, - 515F9503C6F079D663CD9072 /* Repository.swift in Sources */, - 515F94802804A5FEB69837C8 /* PostsUseCase.swift in Sources */, - BCD8C8C81E73475000F79E3E /* RMUser.swift in Sources */, - 515F978F1DBE7C45646004F1 /* Observable+Ext.swift in Sources */, - BCD8C8C01E73471A00F79E3E /* RMComment.swift in Sources */, - BCD8C8C21E73472400F79E3E /* RMCompany.swift in Sources */, - 515F9A39814357F9E05282CD /* RunLoopThreadScheduler.swift in Sources */, - BCD8C8BE1E73470C00F79E3E /* RMAlbum.swift in Sources */, - BCD8C8BC1E73470000F79E3E /* RMAddress.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 25897B711E58BF4600D3563C /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 25897B7D1E58BF4600D3563C /* RealmPlatformTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - BD107F441E7298690043D900 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - BD50EEFF1E7ADDDD00CBEBD4 /* UsersNetwork.swift in Sources */, - BD50EEF91E7ADDA200CBEBD4 /* CommentsNetwork.swift in Sources */, - BD107F751E72B1E20043D900 /* Post+Mapping.swift in Sources */, - BD50EEF71E7ADD5400CBEBD4 /* AlbumsNetwork.swift in Sources */, - 25707C751F23812E00F852F7 /* Cache.swift in Sources */, - BD50EEF51E7AD9A300CBEBD4 /* Network.swift in Sources */, - BD50EEFD1E7ADDC000CBEBD4 /* TodosNetwork.swift in Sources */, - BD107F691E72A0EF0043D900 /* PostsNetwork.swift in Sources */, - BD50EF011E7AF37C00CBEBD4 /* NetworkProvider.swift in Sources */, - BD50EEFB1E7ADDAF00CBEBD4 /* PhotosNetwork.swift in Sources */, - 515F9EA01C8D63D03B41FF8F /* PostsUseCase.swift in Sources */, - 515F9256E79E1689380314E5 /* UseCaseProvider.swift in Sources */, - 25707C711F23802C00F852F7 /* Encodable.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - BD107F4D1E7298690043D900 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - BD107F591E72986A0043D900 /* NetworkTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 25897B161E58BD9100D3563C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 25897AFF1E58BD9100D3563C /* CleanArchitectureRxSwift */; - targetProxy = 25897B151E58BD9100D3563C /* PBXContainerItemProxy */; - }; - 25897B331E58BF0D00D3563C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 25897B271E58BF0D00D3563C /* Domain */; - targetProxy = 25897B321E58BF0D00D3563C /* PBXContainerItemProxy */; - }; - 25897B351E58BF0D00D3563C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 25897AFF1E58BD9100D3563C /* CleanArchitectureRxSwift */; - targetProxy = 25897B341E58BF0D00D3563C /* PBXContainerItemProxy */; - }; - 25897B3C1E58BF0D00D3563C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 25897B271E58BF0D00D3563C /* Domain */; - targetProxy = 25897B3B1E58BF0D00D3563C /* PBXContainerItemProxy */; - }; - 25897B561E58BF3600D3563C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 25897B4A1E58BF3600D3563C /* CoreDataPlatform */; - targetProxy = 25897B551E58BF3600D3563C /* PBXContainerItemProxy */; - }; - 25897B581E58BF3600D3563C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 25897AFF1E58BD9100D3563C /* CleanArchitectureRxSwift */; - targetProxy = 25897B571E58BF3600D3563C /* PBXContainerItemProxy */; - }; - 25897B5F1E58BF3600D3563C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 25897B4A1E58BF3600D3563C /* CoreDataPlatform */; - targetProxy = 25897B5E1E58BF3600D3563C /* PBXContainerItemProxy */; - }; - 25897B781E58BF4600D3563C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 25897B6C1E58BF4600D3563C /* RealmPlatform */; - targetProxy = 25897B771E58BF4600D3563C /* PBXContainerItemProxy */; - }; - 25897B7A1E58BF4600D3563C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 25897AFF1E58BD9100D3563C /* CleanArchitectureRxSwift */; - targetProxy = 25897B791E58BF4600D3563C /* PBXContainerItemProxy */; - }; - 25897B811E58BF4600D3563C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 25897B6C1E58BF4600D3563C /* RealmPlatform */; - targetProxy = 25897B801E58BF4600D3563C /* PBXContainerItemProxy */; - }; - BD107F541E7298690043D900 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = BD107F481E7298690043D900 /* NetworkPlatform */; - targetProxy = BD107F531E7298690043D900 /* PBXContainerItemProxy */; - }; - BD107F561E72986A0043D900 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 25897AFF1E58BD9100D3563C /* CleanArchitectureRxSwift */; - targetProxy = BD107F551E72986A0043D900 /* PBXContainerItemProxy */; - }; - BD107F5D1E72986A0043D900 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = BD107F481E7298690043D900 /* NetworkPlatform */; - targetProxy = BD107F5C1E72986A0043D900 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 25897B071E58BD9100D3563C /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 25897B081E58BD9100D3563C /* Base */, - ); - name = Main.storyboard; - path = .; - sourceTree = ""; - }; - 25897B0C1E58BD9100D3563C /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 25897B0D1E58BD9100D3563C /* Base */, - ); - name = LaunchScreen.storyboard; - path = .; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 25897B1B1E58BD9100D3563C /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.2; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 25897B1C1E58BD9100D3563C /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.2; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 25897B1E1E58BD9100D3563C /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 84A5797E91E6FA5FA24A4896 /* Pods-CleanArchitectureRxSwift.debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = BU4T5Z97JD; - INFOPLIST_FILE = CleanArchitectureRxSwift/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\""; - PRODUCT_BUNDLE_IDENTIFIER = com.sergdort.CleanArchitectureRxSwift; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.2; - }; - name = Debug; - }; - 25897B1F1E58BD9100D3563C /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 3F5B77FEC1EA2332323A0EB9 /* Pods-CleanArchitectureRxSwift.release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = BU4T5Z97JD; - INFOPLIST_FILE = CleanArchitectureRxSwift/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\""; - PRODUCT_BUNDLE_IDENTIFIER = com.sergdort.CleanArchitectureRxSwift; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.2; - }; - name = Release; - }; - 25897B211E58BD9100D3563C /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 3D9EA43A0DD207CAFEADD07C /* Pods-CleanArchitectureRxSwiftTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_ENABLE_MODULES = YES; - INFOPLIST_FILE = CleanArchitectureRxSwiftTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.sergdort.CleanArchitectureRxSwiftTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.2; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CleanArchitectureRxSwift.app/CleanArchitectureRxSwift"; - }; - name = Debug; - }; - 25897B221E58BD9100D3563C /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = A6A063DE6BDC13E3B800BB80 /* Pods-CleanArchitectureRxSwiftTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_ENABLE_MODULES = YES; - INFOPLIST_FILE = CleanArchitectureRxSwiftTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.sergdort.CleanArchitectureRxSwiftTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CleanArchitectureRxSwift.app/CleanArchitectureRxSwift"; - }; - name = Release; - }; - 25897B401E58BF0D00D3563C /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 07BECD54B799E689F7DF0941 /* Pods-Domain.debug.xcconfig */; - buildSettings = { - CODE_SIGN_IDENTITY = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = Domain/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.sergdort.Domain; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - 25897B411E58BF0D00D3563C /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = FA1AB793DF561BEEC8C591F7 /* Pods-Domain.release.xcconfig */; - buildSettings = { - CODE_SIGN_IDENTITY = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = Domain/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.sergdort.Domain; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - 25897B441E58BF0D00D3563C /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = B0092014AEC057C48B9745EA /* Pods-DomainTests.debug.xcconfig */; - buildSettings = { - INFOPLIST_FILE = DomainTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.sergdort.DomainTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CleanArchitectureRxSwift.app/CleanArchitectureRxSwift"; - }; - name = Debug; - }; - 25897B451E58BF0D00D3563C /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9B6A26BB04BBAABE85CB9EDC /* Pods-DomainTests.release.xcconfig */; - buildSettings = { - INFOPLIST_FILE = DomainTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.sergdort.DomainTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CleanArchitectureRxSwift.app/CleanArchitectureRxSwift"; - }; - name = Release; - }; - 25897B631E58BF3600D3563C /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 3058945350D99DC786F3CDC3 /* Pods-CoreDataPlatform.debug.xcconfig */; - buildSettings = { - CODE_SIGN_IDENTITY = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = CoreDataPlatform/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.sergdort.CoreDataPlatform; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - 25897B641E58BF3600D3563C /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 71C4CC5892A6E3601D801729 /* Pods-CoreDataPlatform.release.xcconfig */; - buildSettings = { - CODE_SIGN_IDENTITY = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = CoreDataPlatform/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.sergdort.CoreDataPlatform; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - 25897B661E58BF3600D3563C /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = F1F89859FF9197F25DB915C7 /* Pods-CoreDataPlatformTests.debug.xcconfig */; - buildSettings = { - INFOPLIST_FILE = CoreDataPlatformTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.sergdort.CoreDataPlatformTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CleanArchitectureRxSwift.app/CleanArchitectureRxSwift"; - }; - name = Debug; - }; - 25897B671E58BF3600D3563C /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 2BBB44DCB902E459F05D68B0 /* Pods-CoreDataPlatformTests.release.xcconfig */; - buildSettings = { - INFOPLIST_FILE = CoreDataPlatformTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.sergdort.CoreDataPlatformTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CleanArchitectureRxSwift.app/CleanArchitectureRxSwift"; - }; - name = Release; - }; - 25897B851E58BF4600D3563C /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 4C9D6A2833A962FEF238F391 /* Pods-RealmPlatform.debug.xcconfig */; - buildSettings = { - CODE_SIGN_IDENTITY = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = RealmPlatform/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.sergdort.RealmPlatform; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - 25897B861E58BF4600D3563C /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 081D23F2DCF9DBA930AB0FE7 /* Pods-RealmPlatform.release.xcconfig */; - buildSettings = { - CODE_SIGN_IDENTITY = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = RealmPlatform/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.sergdort.RealmPlatform; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - 25897B881E58BF4600D3563C /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = A8E1F5AE93A531609690A036 /* Pods-RealmPlatformTests.debug.xcconfig */; - buildSettings = { - INFOPLIST_FILE = RealmPlatformTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.sergdort.RealmPlatformTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CleanArchitectureRxSwift.app/CleanArchitectureRxSwift"; - }; - name = Debug; - }; - 25897B891E58BF4600D3563C /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 0D4A3B45078CD8ACCC1314EC /* Pods-RealmPlatformTests.release.xcconfig */; - buildSettings = { - INFOPLIST_FILE = RealmPlatformTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.sergdort.RealmPlatformTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CleanArchitectureRxSwift.app/CleanArchitectureRxSwift"; - }; - name = Release; - }; - BD107F601E72986A0043D900 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 81E26E9003A18D1FDD392578 /* Pods-NetworkPlatform.debug.xcconfig */; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = "$(SRCROOT)/NetworkPlatform/Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.ayastrebov.Network; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - BD107F611E72986A0043D900 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 4ED44AC018F010F22DC2E0BD /* Pods-NetworkPlatform.release.xcconfig */; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = "$(SRCROOT)/NetworkPlatform/Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.ayastrebov.Network; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - BD107F621E72986A0043D900 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = BC15FAFCFFF7EB7017614B45 /* Pods-NetworkPlatformTests.debug.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = NetworkTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.ayastrebov.NetworkTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CleanArchitectureRxSwift.app/CleanArchitectureRxSwift"; - }; - name = Debug; - }; - BD107F631E72986A0043D900 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 426939EA03F41A859CEFCA16 /* Pods-NetworkPlatformTests.release.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = NetworkTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.ayastrebov.NetworkTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CleanArchitectureRxSwift.app/CleanArchitectureRxSwift"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 25897AFB1E58BD9100D3563C /* Build configuration list for PBXProject "CleanArchitectureRxSwift" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 25897B1B1E58BD9100D3563C /* Debug */, - 25897B1C1E58BD9100D3563C /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 25897B1D1E58BD9100D3563C /* Build configuration list for PBXNativeTarget "CleanArchitectureRxSwift" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 25897B1E1E58BD9100D3563C /* Debug */, - 25897B1F1E58BD9100D3563C /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 25897B201E58BD9100D3563C /* Build configuration list for PBXNativeTarget "CleanArchitectureRxSwiftTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 25897B211E58BD9100D3563C /* Debug */, - 25897B221E58BD9100D3563C /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 25897B3F1E58BF0D00D3563C /* Build configuration list for PBXNativeTarget "Domain" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 25897B401E58BF0D00D3563C /* Debug */, - 25897B411E58BF0D00D3563C /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 25897B431E58BF0D00D3563C /* Build configuration list for PBXNativeTarget "DomainTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 25897B441E58BF0D00D3563C /* Debug */, - 25897B451E58BF0D00D3563C /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 25897B621E58BF3600D3563C /* Build configuration list for PBXNativeTarget "CoreDataPlatform" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 25897B631E58BF3600D3563C /* Debug */, - 25897B641E58BF3600D3563C /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 25897B651E58BF3600D3563C /* Build configuration list for PBXNativeTarget "CoreDataPlatformTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 25897B661E58BF3600D3563C /* Debug */, - 25897B671E58BF3600D3563C /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 25897B841E58BF4600D3563C /* Build configuration list for PBXNativeTarget "RealmPlatform" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 25897B851E58BF4600D3563C /* Debug */, - 25897B861E58BF4600D3563C /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 25897B871E58BF4600D3563C /* Build configuration list for PBXNativeTarget "RealmPlatformTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 25897B881E58BF4600D3563C /* Debug */, - 25897B891E58BF4600D3563C /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - BD107F641E72986A0043D900 /* Build configuration list for PBXNativeTarget "NetworkPlatform" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - BD107F601E72986A0043D900 /* Debug */, - BD107F611E72986A0043D900 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - BD107F651E72986A0043D900 /* Build configuration list for PBXNativeTarget "NetworkPlatformTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - BD107F621E72986A0043D900 /* Debug */, - BD107F631E72986A0043D900 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - -/* Begin XCVersionGroup section */ - 25897BAA1E58C25500D3563C /* Model.xcdatamodeld */ = { - isa = XCVersionGroup; - children = ( - 25897BAB1E58C25500D3563C /* Model.xcdatamodel */, - ); - currentVersion = 25897BAB1E58C25500D3563C /* Model.xcdatamodel */; - path = Model.xcdatamodeld; - sourceTree = ""; - versionGroupType = wrapper.xcdatamodel; - }; -/* End XCVersionGroup section */ - }; - rootObject = 25897AF81E58BD9100D3563C /* Project object */; -} diff --git a/CleanArchitectureRxSwift.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/CleanArchitectureRxSwift.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index cf3944af..00000000 --- a/CleanArchitectureRxSwift.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/CleanArchitectureRxSwift.xcworkspace/contents.xcworkspacedata b/CleanArchitectureRxSwift.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index b790726c..00000000 --- a/CleanArchitectureRxSwift.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/CleanArchitectureRxSwift.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/CleanArchitectureRxSwift.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/CleanArchitectureRxSwift.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/CleanArchitectureRxSwift/AppDelegate.swift b/CleanArchitectureRxSwift/AppDelegate.swift deleted file mode 100644 index fd99055f..00000000 --- a/CleanArchitectureRxSwift/AppDelegate.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// AppDelegate.swift -// CleanArchitectureRxSwift -// -// Created by sergdort on 18/02/2017. -// Copyright © 2017 sergdort. All rights reserved. -// - -import UIKit - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - - var window: UIWindow? - - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - let window = UIWindow(frame: UIScreen.main.bounds) - - Application.shared.configureMainInterface(in: window) - - self.window = window - return true - } - -} diff --git a/CleanArchitectureRxSwift/Application/Application.swift b/CleanArchitectureRxSwift/Application/Application.swift deleted file mode 100644 index 1c9ec783..00000000 --- a/CleanArchitectureRxSwift/Application/Application.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Foundation -import Domain -import NetworkPlatform -import CoreDataPlatform -import RealmPlatform - -final class Application { - static let shared = Application() - - private let coreDataUseCaseProvider: Domain.UseCaseProvider - private let realmUseCaseProvider: Domain.UseCaseProvider - private let networkUseCaseProvider: Domain.UseCaseProvider - - private init() { - self.coreDataUseCaseProvider = CoreDataPlatform.UseCaseProvider() - self.realmUseCaseProvider = RealmPlatform.UseCaseProvider() - self.networkUseCaseProvider = NetworkPlatform.UseCaseProvider() - } - - func configureMainInterface(in window: UIWindow) { - let storyboard = UIStoryboard(name: "Main", bundle: nil) - let cdNavigationController = UINavigationController() - cdNavigationController.tabBarItem = UITabBarItem(title: "CoreData", - image: UIImage(named: "Box"), - selectedImage: nil) - let cdNavigator = DefaultPostsNavigator(services: coreDataUseCaseProvider, - navigationController: cdNavigationController, - storyBoard: storyboard) - - let rmNavigationController = UINavigationController() - rmNavigationController.tabBarItem = UITabBarItem(title: "Realm", - image: UIImage(named: "Toolbox"), - selectedImage: nil) - let rmNavigator = DefaultPostsNavigator(services: realmUseCaseProvider, - navigationController: rmNavigationController, - storyBoard: storyboard) - - let networkNavigationController = UINavigationController() - networkNavigationController.tabBarItem = UITabBarItem(title: "Network", - image: UIImage(named: "Toolbox"), - selectedImage: nil) - let networkNavigator = DefaultPostsNavigator(services: networkUseCaseProvider, - navigationController: networkNavigationController, - storyBoard: storyboard) - - let tabBarController = UITabBarController() - tabBarController.viewControllers = [ - cdNavigationController, - rmNavigationController, - networkNavigationController - ] - window.rootViewController = tabBarController - - cdNavigator.toPosts() - rmNavigator.toPosts() - networkNavigator.toPosts() - } -} diff --git a/CleanArchitectureRxSwift/Assets.xcassets/AppIcon.appiconset/Contents.json b/CleanArchitectureRxSwift/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index b8236c65..00000000 --- a/CleanArchitectureRxSwift/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/CleanArchitectureRxSwift/Assets.xcassets/Box.imageset/Box_30_2.pdf b/CleanArchitectureRxSwift/Assets.xcassets/Box.imageset/Box_30_2.pdf deleted file mode 100644 index 5cafad78..00000000 Binary files a/CleanArchitectureRxSwift/Assets.xcassets/Box.imageset/Box_30_2.pdf and /dev/null differ diff --git a/CleanArchitectureRxSwift/Assets.xcassets/Box.imageset/Contents.json b/CleanArchitectureRxSwift/Assets.xcassets/Box.imageset/Contents.json deleted file mode 100644 index 8db5c6bf..00000000 --- a/CleanArchitectureRxSwift/Assets.xcassets/Box.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "Box_30_2.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "template-rendering-intent" : "template" - } -} \ No newline at end of file diff --git a/CleanArchitectureRxSwift/Assets.xcassets/Contents.json b/CleanArchitectureRxSwift/Assets.xcassets/Contents.json deleted file mode 100644 index da4a164c..00000000 --- a/CleanArchitectureRxSwift/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/CleanArchitectureRxSwift/Assets.xcassets/Toolbox.imageset/Contents.json b/CleanArchitectureRxSwift/Assets.xcassets/Toolbox.imageset/Contents.json deleted file mode 100644 index 9614397b..00000000 --- a/CleanArchitectureRxSwift/Assets.xcassets/Toolbox.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "Toolbox_30_2.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "template-rendering-intent" : "template" - } -} \ No newline at end of file diff --git a/CleanArchitectureRxSwift/Assets.xcassets/Toolbox.imageset/Toolbox_30_2.pdf b/CleanArchitectureRxSwift/Assets.xcassets/Toolbox.imageset/Toolbox_30_2.pdf deleted file mode 100644 index f668bc7e..00000000 Binary files a/CleanArchitectureRxSwift/Assets.xcassets/Toolbox.imageset/Toolbox_30_2.pdf and /dev/null differ diff --git a/CleanArchitectureRxSwift/Base.lproj/Main.storyboard b/CleanArchitectureRxSwift/Base.lproj/Main.storyboard deleted file mode 100644 index 7bb25b97..00000000 --- a/CleanArchitectureRxSwift/Base.lproj/Main.storyboard +++ /dev/null @@ -1,245 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/CleanArchitectureRxSwift/Common/ViewModelType.swift b/CleanArchitectureRxSwift/Common/ViewModelType.swift deleted file mode 100644 index 1c3d691e..00000000 --- a/CleanArchitectureRxSwift/Common/ViewModelType.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation - -protocol ViewModelType { - associatedtype Input - associatedtype Output - - func transform(input: Input) -> Output -} diff --git a/CleanArchitectureRxSwift/Scenes/AllPosts/PostItemViewModel.swift b/CleanArchitectureRxSwift/Scenes/AllPosts/PostItemViewModel.swift deleted file mode 100644 index 944ef8fb..00000000 --- a/CleanArchitectureRxSwift/Scenes/AllPosts/PostItemViewModel.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// PostItemViewModel.swift -// CleanArchitectureRxSwift -// -// Created by Stefano Mondino on 09/08/17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Foundation -import Domain - -final class PostItemViewModel { - let title:String - let subtitle : String - let post: Post - init (with post:Post) { - self.post = post - self.title = post.title.uppercased() - self.subtitle = post.body - } -} diff --git a/CleanArchitectureRxSwift/Scenes/AllPosts/PostTableViewCell.swift b/CleanArchitectureRxSwift/Scenes/AllPosts/PostTableViewCell.swift deleted file mode 100644 index f9b95f64..00000000 --- a/CleanArchitectureRxSwift/Scenes/AllPosts/PostTableViewCell.swift +++ /dev/null @@ -1,12 +0,0 @@ -import UIKit - -final class PostTableViewCell: UITableViewCell { - @IBOutlet weak var titleLabel: UILabel! - @IBOutlet weak var detailsLabel: UILabel! - - func bind(_ viewModel:PostItemViewModel) { - self.titleLabel.text = viewModel.title - self.detailsLabel.text = viewModel.subtitle - } - -} diff --git a/CleanArchitectureRxSwift/Scenes/AllPosts/PostsNavigator.swift b/CleanArchitectureRxSwift/Scenes/AllPosts/PostsNavigator.swift deleted file mode 100644 index c5fcbee3..00000000 --- a/CleanArchitectureRxSwift/Scenes/AllPosts/PostsNavigator.swift +++ /dev/null @@ -1,47 +0,0 @@ -import UIKit -import Domain - -protocol PostsNavigator { - func toCreatePost() - func toPost(_ post: Post) - func toPosts() -} - -class DefaultPostsNavigator: PostsNavigator { - private let storyBoard: UIStoryboard - private let navigationController: UINavigationController - private let services: UseCaseProvider - - init(services: UseCaseProvider, - navigationController: UINavigationController, - storyBoard: UIStoryboard) { - self.services = services - self.navigationController = navigationController - self.storyBoard = storyBoard - } - - func toPosts() { - let vc = storyBoard.instantiateViewController(ofType: PostsViewController.self) - vc.viewModel = PostsViewModel(useCase: services.makePostsUseCase(), - navigator: self) - navigationController.pushViewController(vc, animated: true) - } - - func toCreatePost() { - let navigator = DefaultCreatePostNavigator(navigationController: navigationController) - let viewModel = CreatePostViewModel(createPostUseCase: services.makePostsUseCase(), - navigator: navigator) - let vc = storyBoard.instantiateViewController(ofType: CreatePostViewController.self) - vc.viewModel = viewModel - let nc = UINavigationController(rootViewController: vc) - navigationController.present(nc, animated: true, completion: nil) - } - - func toPost(_ post: Post) { - let navigator = DefaultEditPostNavigator(navigationController: navigationController) - let viewModel = EditPostViewModel(post: post, useCase: services.makePostsUseCase(), navigator: navigator) - let vc = storyBoard.instantiateViewController(ofType: EditPostViewController.self) - vc.viewModel = viewModel - navigationController.pushViewController(vc, animated: true) - } -} diff --git a/CleanArchitectureRxSwift/Scenes/AllPosts/PostsViewController.swift b/CleanArchitectureRxSwift/Scenes/AllPosts/PostsViewController.swift deleted file mode 100644 index b2bc63d2..00000000 --- a/CleanArchitectureRxSwift/Scenes/AllPosts/PostsViewController.swift +++ /dev/null @@ -1,57 +0,0 @@ -import UIKit -import Domain -import RxSwift -import RxCocoa - -class PostsViewController: UIViewController { - private let disposeBag = DisposeBag() - - var viewModel: PostsViewModel! - @IBOutlet weak var tableView: UITableView! - @IBOutlet weak var createPostButton: UIBarButtonItem! - - override func viewDidLoad() { - super.viewDidLoad() - configureTableView() - bindViewModel() - } - - private func configureTableView() { - tableView.refreshControl = UIRefreshControl() - tableView.estimatedRowHeight = 64 - tableView.rowHeight = UITableView.automaticDimension - } - - private func bindViewModel() { - assert(viewModel != nil) - let viewWillAppear = rx.sentMessage(#selector(UIViewController.viewWillAppear(_:))) - .mapToVoid() - .asDriverOnErrorJustComplete() - let pull = tableView.refreshControl!.rx - .controlEvent(.valueChanged) - .asDriver() - - let input = PostsViewModel.Input(trigger: Driver.merge(viewWillAppear, pull), - createPostTrigger: createPostButton.rx.tap.asDriver(), - selection: tableView.rx.itemSelected.asDriver()) - let output = viewModel.transform(input: input) - //Bind Posts to UITableView - output.posts.drive(tableView.rx.items(cellIdentifier: PostTableViewCell.reuseID, cellType: PostTableViewCell.self)) { tv, viewModel, cell in - cell.bind(viewModel) - }.disposed(by: disposeBag) - //Connect Create Post to UI - - output.fetching - .drive(tableView.refreshControl!.rx.isRefreshing) - .disposed(by: disposeBag) - output.createPost - .drive() - .disposed(by: disposeBag) - output.selectedPost - .drive() - .disposed(by: disposeBag) - } -} - - - diff --git a/CleanArchitectureRxSwift/Scenes/AllPosts/PostsViewModel.swift b/CleanArchitectureRxSwift/Scenes/AllPosts/PostsViewModel.swift deleted file mode 100644 index 983f370c..00000000 --- a/CleanArchitectureRxSwift/Scenes/AllPosts/PostsViewModel.swift +++ /dev/null @@ -1,56 +0,0 @@ -import Foundation -import Domain -import RxSwift -import RxCocoa - -final class PostsViewModel: ViewModelType { - - struct Input { - let trigger: Driver - let createPostTrigger: Driver - let selection: Driver - } - struct Output { - let fetching: Driver - let posts: Driver<[PostItemViewModel]> - let createPost: Driver - let selectedPost: Driver - let error: Driver - } - - private let useCase: PostsUseCase - private let navigator: PostsNavigator - - init(useCase: PostsUseCase, navigator: PostsNavigator) { - self.useCase = useCase - self.navigator = navigator - } - - func transform(input: Input) -> Output { - let activityIndicator = ActivityIndicator() - let errorTracker = ErrorTracker() - let posts = input.trigger.flatMapLatest { - return self.useCase.posts() - .trackActivity(activityIndicator) - .trackError(errorTracker) - .asDriverOnErrorJustComplete() - .map { $0.map { PostItemViewModel(with: $0) } } - } - - let fetching = activityIndicator.asDriver() - let errors = errorTracker.asDriver() - let selectedPost = input.selection - .withLatestFrom(posts) { (indexPath, posts) -> Post in - return posts[indexPath.row].post - } - .do(onNext: navigator.toPost) - let createPost = input.createPostTrigger - .do(onNext: navigator.toCreatePost) - - return Output(fetching: fetching, - posts: posts, - createPost: createPost, - selectedPost: selectedPost, - error: errors) - } -} diff --git a/CleanArchitectureRxSwift/Scenes/CreatePost/CreatePostNavigator.swift b/CleanArchitectureRxSwift/Scenes/CreatePost/CreatePostNavigator.swift deleted file mode 100644 index 511e0094..00000000 --- a/CleanArchitectureRxSwift/Scenes/CreatePost/CreatePostNavigator.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// Created by sergdort on 19/02/2017. -// Copyright (c) 2017 sergdort. All rights reserved. -// - - -import Foundation -import UIKit -import Domain - -protocol CreatePostNavigator { - - func toPosts() -} - -final class DefaultCreatePostNavigator: CreatePostNavigator { - private let navigationController: UINavigationController - - init(navigationController: UINavigationController) { - self.navigationController = navigationController - } - - func toPosts() { - navigationController.dismiss(animated: true) - } -} diff --git a/CleanArchitectureRxSwift/Scenes/CreatePost/CreatePostViewController.swift b/CleanArchitectureRxSwift/Scenes/CreatePost/CreatePostViewController.swift deleted file mode 100644 index 639d139a..00000000 --- a/CleanArchitectureRxSwift/Scenes/CreatePost/CreatePostViewController.swift +++ /dev/null @@ -1,30 +0,0 @@ -import UIKit -import Domain -import RxSwift -import RxCocoa - -final class CreatePostViewController: UIViewController { - private let disposeBag = DisposeBag() - - var viewModel: CreatePostViewModel! - - @IBOutlet weak var cancelButton: UIBarButtonItem! - @IBOutlet weak var saveButton: UIBarButtonItem! - @IBOutlet weak var titleTextField: UITextField! - @IBOutlet weak var detailsTextView: UITextView! - - override func viewDidLoad() { - super.viewDidLoad() - let input = CreatePostViewModel.Input(cancelTrigger: cancelButton.rx.tap.asDriver(), - saveTrigger: saveButton.rx.tap.asDriver(), - title: titleTextField.rx.text.orEmpty.asDriver(), - details: detailsTextView.rx.text.orEmpty.asDriver()) - - let output = viewModel.transform(input: input) - - output.dismiss.drive() - .disposed(by: disposeBag) - output.saveEnabled.drive(saveButton.rx.isEnabled) - .disposed(by: disposeBag) - } -} diff --git a/CleanArchitectureRxSwift/Scenes/CreatePost/CreatePostViewModel.swift b/CleanArchitectureRxSwift/Scenes/CreatePost/CreatePostViewModel.swift deleted file mode 100644 index d9268221..00000000 --- a/CleanArchitectureRxSwift/Scenes/CreatePost/CreatePostViewModel.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// Created by sergdort on 19/02/2017. -// Copyright (c) 2017 sergdort. All rights reserved. -// - - -import Foundation -import RxSwift -import RxCocoa -import Domain - -final class CreatePostViewModel: ViewModelType { - private let createPostUseCase: PostsUseCase - private let navigator: CreatePostNavigator - - init(createPostUseCase: PostsUseCase, navigator: CreatePostNavigator) { - self.createPostUseCase = createPostUseCase - self.navigator = navigator - } - - func transform(input: Input) -> Output { - let titleAndDetails = Driver.combineLatest(input.title, input.details) - let activityIndicator = ActivityIndicator() - - let canSave = Driver.combineLatest(titleAndDetails, activityIndicator.asDriver()) { - return !$0.0.isEmpty && !$0.1.isEmpty && !$1 - } - - let save = input.saveTrigger.withLatestFrom(titleAndDetails) - .map { (title, content) in - return Post(body: content, title: title) - } - .flatMapLatest { [unowned self] in - return self.createPostUseCase.save(post: $0) - .trackActivity(activityIndicator) - .asDriverOnErrorJustComplete() - } - - let dismiss = Driver.of(save, input.cancelTrigger) - .merge() - .do(onNext: navigator.toPosts) - - return Output(dismiss: dismiss, saveEnabled: canSave) - } -} - -extension CreatePostViewModel { - struct Input { - let cancelTrigger: Driver - let saveTrigger: Driver - let title: Driver - let details: Driver - } - - struct Output { - let dismiss: Driver - let saveEnabled: Driver - } -} diff --git a/CleanArchitectureRxSwift/Scenes/EditPost/EditPostNavigator.swift b/CleanArchitectureRxSwift/Scenes/EditPost/EditPostNavigator.swift deleted file mode 100644 index 4b6f4db8..00000000 --- a/CleanArchitectureRxSwift/Scenes/EditPost/EditPostNavigator.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation -import UIKit -import Domain - -protocol EditPostNavigator { - func toPosts() -} - -final class DefaultEditPostNavigator: EditPostNavigator { - private let navigationController: UINavigationController - - init(navigationController: UINavigationController) { - self.navigationController = navigationController - } - - func toPosts() { - navigationController.popViewController(animated: true) - } -} diff --git a/CleanArchitectureRxSwift/Scenes/EditPost/EditPostViewController.swift b/CleanArchitectureRxSwift/Scenes/EditPost/EditPostViewController.swift deleted file mode 100644 index 07fd83dc..00000000 --- a/CleanArchitectureRxSwift/Scenes/EditPost/EditPostViewController.swift +++ /dev/null @@ -1,86 +0,0 @@ -import UIKit -import RxSwift -import RxCocoa -import Domain - -final class EditPostViewController: UIViewController { - private let disposeBag = DisposeBag() - - @IBOutlet weak var editButton: UIBarButtonItem! - @IBOutlet weak var deleteButton: UIBarButtonItem! - @IBOutlet weak var titleTextField: UITextField! - @IBOutlet weak var detailsTextView: UITextView! - - var viewModel: EditPostViewModel! - - override func viewDidLoad() { - super.viewDidLoad() - - let deleteTrigger = deleteButton.rx.tap.flatMap { - return Observable.create { observer in - - let alert = UIAlertController(title: "Delete Post", - message: "Are you sure you want to delete this post?", - preferredStyle: .alert - ) - let yesAction = UIAlertAction(title: "Yes", style: .destructive, handler: { _ -> () in observer.onNext(()) }) - let noAction = UIAlertAction(title: "No", style: .cancel, handler: { _ -> () in observer.onNext(()) }) - alert.addAction(yesAction) - alert.addAction(noAction) - - self.present(alert, animated: true, completion: nil) - - return Disposables.create() - } - } - - let input = EditPostViewModel.Input( - editTrigger: editButton.rx.tap.asDriver(), - deleteTrigger: deleteTrigger.asDriverOnErrorJustComplete(), - title: titleTextField.rx.text.orEmpty.asDriver(), - details: detailsTextView.rx.text.orEmpty.asDriver() - ) - - let output = viewModel.transform(input: input) - - [output.editButtonTitle.drive(editButton.rx.title), - output.editing.drive(titleTextField.rx.isEnabled), - output.editing.drive(detailsTextView.rx.isEditable), - output.post.drive(postBinding), - output.save.drive(), - output.error.drive(errorBinding), - output.delete.drive()] - .forEach({$0.disposed(by: disposeBag)}) - } - - var postBinding: UIBindingObserver { - return UIBindingObserver(UIElement: self, binding: { (vc, post) in - vc.titleTextField.text = post.title - vc.detailsTextView.text = post.body - vc.title = post.title - }) - } - - var errorBinding: UIBindingObserver { - return UIBindingObserver(UIElement: self, binding: { (vc, _) in - let alert = UIAlertController(title: "Save Error", - message: "Something went wrong", - preferredStyle: .alert) - let action = UIAlertAction(title: "Dismiss", - style: UIAlertAction.Style.cancel, - handler: nil) - alert.addAction(action) - vc.present(alert, animated: true, completion: nil) - }) - } -} - - - -extension Reactive where Base: UITextView { - var isEditable: UIBindingObserver { - return UIBindingObserver(UIElement: self.base, binding: { (textView, isEditable) in - textView.isEditable = isEditable - }) - } -} diff --git a/CleanArchitectureRxSwift/Scenes/EditPost/EditPostViewModel.swift b/CleanArchitectureRxSwift/Scenes/EditPost/EditPostViewModel.swift deleted file mode 100644 index 982f75a1..00000000 --- a/CleanArchitectureRxSwift/Scenes/EditPost/EditPostViewModel.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Domain -import RxSwift -import RxCocoa - -final class EditPostViewModel: ViewModelType { - private let post: Post - private let useCase: PostsUseCase - private let navigator: EditPostNavigator - - init(post: Post, useCase: PostsUseCase, navigator: EditPostNavigator) { - self.post = post - self.useCase = useCase - self.navigator = navigator - } - - func transform(input: Input) -> Output { - let errorTracker = ErrorTracker() - let editing = input.editTrigger.scan(false) { editing, _ in - return !editing - }.startWith(false) - - let saveTrigger = editing.skip(1) //we dont need initial state - .filter { $0 == false } - .mapToVoid() - let titleAndDetails = Driver.combineLatest(input.title, input.details) - let post = Driver.combineLatest(Driver.just(self.post), titleAndDetails) { (post, titleAndDetails) -> Post in - return Post(body: titleAndDetails.1, title: titleAndDetails.0, uid: post.uid, userId: post.userId, createdAt: post.createdAt) - }.startWith(self.post) - let editButtonTitle = editing.map { editing -> String in - return editing == true ? "Save" : "Edit" - } - let savePost = saveTrigger.withLatestFrom(post) - .flatMapLatest { post in - return self.useCase.save(post: post) - .trackError(errorTracker) - .asDriverOnErrorJustComplete() - } - - let deletePost = input.deleteTrigger.withLatestFrom(post) - .flatMapLatest { post in - return self.useCase.delete(post: post) - .trackError(errorTracker) - .asDriverOnErrorJustComplete() - }.do(onNext: { - self.navigator.toPosts() - }) - - return Output(editButtonTitle: editButtonTitle, - save: savePost, - delete: deletePost, - editing: editing, - post: post, - error: errorTracker.asDriver()) - } -} - -extension EditPostViewModel { - struct Input { - let editTrigger: Driver - let deleteTrigger: Driver - let title: Driver - let details: Driver - } - - struct Output { - let editButtonTitle: Driver - let save: Driver - let delete: Driver - let editing: Driver - let post: Driver - let error: Driver - } -} diff --git a/CleanArchitectureRxSwift/Utility/ActivityIndicator.swift b/CleanArchitectureRxSwift/Utility/ActivityIndicator.swift deleted file mode 100644 index 6686afb8..00000000 --- a/CleanArchitectureRxSwift/Utility/ActivityIndicator.swift +++ /dev/null @@ -1,50 +0,0 @@ -import Foundation -import RxSwift -import RxCocoa - -public class ActivityIndicator: SharedSequenceConvertibleType { - public typealias E = Bool - public typealias SharingStrategy = DriverSharingStrategy - - private let _lock = NSRecursiveLock() - private let _variable = Variable(false) - private let _loading: SharedSequence - - public init() { - _loading = _variable.asDriver() - .distinctUntilChanged() - } - - fileprivate func trackActivityOfObservable(_ source: O) -> Observable { - return source.asObservable() - .do(onNext: { _ in - self.sendStopLoading() - }, onError: { _ in - self.sendStopLoading() - }, onCompleted: { - self.sendStopLoading() - }, onSubscribe: subscribed) - } - - private func subscribed() { - _lock.lock() - _variable.value = true - _lock.unlock() - } - - private func sendStopLoading() { - _lock.lock() - _variable.value = false - _lock.unlock() - } - - public func asSharedSequence() -> SharedSequence { - return _loading - } -} - -extension ObservableConvertibleType { - public func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable { - return activityIndicator.trackActivityOfObservable(self) - } -} diff --git a/CleanArchitectureRxSwift/Utility/ErrorTracker.swift b/CleanArchitectureRxSwift/Utility/ErrorTracker.swift deleted file mode 100644 index 4dc5ee89..00000000 --- a/CleanArchitectureRxSwift/Utility/ErrorTracker.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// Created by sergdort on 03/02/2017. -// Copyright (c) 2017 sergdort. All rights reserved. -// - - -import Foundation -import RxSwift -import RxCocoa - -final class ErrorTracker: SharedSequenceConvertibleType { - typealias SharingStrategy = DriverSharingStrategy - private let _subject = PublishSubject() - - func trackError(from source: O) -> Observable { - return source.asObservable().do(onError: onError) - } - - func asSharedSequence() -> SharedSequence { - return _subject.asObservable().asDriverOnErrorJustComplete() - } - - func asObservable() -> Observable { - return _subject.asObservable() - } - - private func onError(_ error: Error) { - _subject.onNext(error) - } - - deinit { - _subject.onCompleted() - } -} - -extension ObservableConvertibleType { - func trackError(_ errorTracker: ErrorTracker) -> Observable { - return errorTracker.trackError(from: self) - } -} diff --git a/CleanArchitectureRxSwift/Utility/Observable+Ext.swift b/CleanArchitectureRxSwift/Utility/Observable+Ext.swift deleted file mode 100644 index 3c942149..00000000 --- a/CleanArchitectureRxSwift/Utility/Observable+Ext.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation -import RxSwift -import RxCocoa - -extension ObservableType where E == Bool { - /// Boolean not operator - public func not() -> Observable { - return self.map(!) - } - -} - -extension SharedSequenceConvertibleType { - func mapToVoid() -> SharedSequence { - return map { _ in } - } -} - -extension ObservableType { - - func catchErrorJustComplete() -> Observable { - return catchError { _ in - return Observable.empty() - } - } - - func asDriverOnErrorJustComplete() -> Driver { - return asDriver { error in - return Driver.empty() - } - } - - func mapToVoid() -> Observable { - return map { _ in } - } -} diff --git a/CleanArchitectureRxSwift/Utility/Reusable.swift b/CleanArchitectureRxSwift/Utility/Reusable.swift deleted file mode 100644 index e50e4653..00000000 --- a/CleanArchitectureRxSwift/Utility/Reusable.swift +++ /dev/null @@ -1,34 +0,0 @@ -import UIKit - -protocol Reusable { - static var reuseID: String {get} -} - -extension Reusable { - static var reuseID: String { - return String(describing: self) - } -} - -extension UITableViewCell: Reusable {} - -extension UIViewController: Reusable {} - -extension UITableView { - func dequeueReusableCell(ofType cellType: T.Type = T.self, at indexPath: IndexPath) -> T where T: UITableViewCell { - guard let cell = dequeueReusableCell(withIdentifier: cellType.reuseID, - for: indexPath) as? T else { - fatalError() - } - return cell - } -} - -extension UIStoryboard { - func instantiateViewController(ofType type: T.Type = T.self) -> T where T: UIViewController { - guard let viewController = instantiateViewController(withIdentifier: type.reuseID) as? T else { - fatalError() - } - return viewController - } -} diff --git a/CleanArchitectureRxSwiftTests/Info.plist b/CleanArchitectureRxSwiftTests/Info.plist deleted file mode 100644 index 6c6c23c4..00000000 --- a/CleanArchitectureRxSwiftTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/CleanArchitectureRxSwiftTests/Scenes/AllPosts/AllPostsUseCaseMock.swift b/CleanArchitectureRxSwiftTests/Scenes/AllPosts/AllPostsUseCaseMock.swift deleted file mode 100644 index 2b8c69ee..00000000 --- a/CleanArchitectureRxSwiftTests/Scenes/AllPosts/AllPostsUseCaseMock.swift +++ /dev/null @@ -1,14 +0,0 @@ -@testable import CleanArchitectureRxSwift -import RxSwift -import Domain - -class AllPostsUseCaseMock: Domain.AllPostsUseCase { - - var posts_ReturnValue: Observable<[Post]> = Observable.just([]) - var posts_Called = false - - func posts() -> Observable<[Post]> { - posts_Called = true - return posts_ReturnValue - } -} diff --git a/CleanArchitectureRxSwiftTests/Scenes/AllPosts/PostsNavigatorMock.swift b/CleanArchitectureRxSwiftTests/Scenes/AllPosts/PostsNavigatorMock.swift deleted file mode 100644 index 0f84cac3..00000000 --- a/CleanArchitectureRxSwiftTests/Scenes/AllPosts/PostsNavigatorMock.swift +++ /dev/null @@ -1,27 +0,0 @@ -@testable import CleanArchitectureRxSwift -import Domain -import RxSwift - -class PostNavigatorMock: PostsNavigator { - - var toPosts_Called = false - - func toPosts() { - toPosts_Called = true - } - - var toCreatePost_Called = false - - func toCreatePost() { - toCreatePost_Called = true - } - - var toPost_post_Called = false - var toPost_post_ReceivedArguments: Post? - - func toPost(_ post: Post) { - toPost_post_Called = true - toPost_post_ReceivedArguments = post - } - -} diff --git a/CleanArchitectureRxSwiftTests/Scenes/AllPosts/PostsViewModelTests.swift b/CleanArchitectureRxSwiftTests/Scenes/AllPosts/PostsViewModelTests.swift deleted file mode 100644 index 6ace744b..00000000 --- a/CleanArchitectureRxSwiftTests/Scenes/AllPosts/PostsViewModelTests.swift +++ /dev/null @@ -1,144 +0,0 @@ -@testable import CleanArchitectureRxSwift -import Domain -import XCTest -import RxSwift -import RxCocoa -import RxBlocking - -enum TestError: Error { - case test -} - -class PostsViewModelTests: XCTestCase { - - var allPostUseCase: AllPostsUseCaseMock! - var postsNavigator: PostNavigatorMock! - var viewModel: PostsViewModel! - - let disposeBag = DisposeBag() - - override func setUp() { - super.setUp() - - allPostUseCase = AllPostsUseCaseMock() - postsNavigator = PostNavigatorMock() - - viewModel = PostsViewModel(useCase: allPostUseCase, - navigator: postsNavigator) - } - - func test_transform_triggerInvoked_postEmited() { - // arrange - let trigger = PublishSubject() - let input = createInput(trigger: trigger) - let output = viewModel.transform(input: input) - - // act - output.posts.drive().disposed(by: disposeBag) - trigger.onNext() - - // assert - XCTAssert(allPostUseCase.posts_Called) - } - - - func test_transform_sendPost_trackFetching() { - // arrange - let trigger = PublishSubject() - let output = viewModel.transform(input: createInput(trigger: trigger)) - let expectedFetching = [true, false] - var actualFetching: [Bool] = [] - - // act - output.fetching - .do(onNext: { actualFetching.append($0) }, - onSubscribe: { actualFetching.append(true) }) - .drive() - .disposed(by: disposeBag) - trigger.onNext() - - // assert - XCTAssertEqual(actualFetching, expectedFetching) - } - - func test_transform_postEmitError_trackError() { - // arrange - let trigger = PublishSubject() - let output = viewModel.transform(input: createInput(trigger: trigger)) - allPostUseCase.posts_ReturnValue = Observable.error(TestError.test) - - // act - output.posts.drive().disposed(by: disposeBag) - output.error.drive().disposed(by: disposeBag) - trigger.onNext() - let error = try! output.error.toBlocking().first() - - // assert - XCTAssertNotNil(error) - } - - func test_transform_triggerInvoked_mapPostsToViewModels() { - // arrange - let trigger = PublishSubject() - let output = viewModel.transform(input: createInput(trigger: trigger)) - allPostUseCase.posts_ReturnValue = Observable.just(createPosts()) - - // act - output.posts.drive().disposed(by: disposeBag) - trigger.onNext() - let posts = try! output.posts.toBlocking().first()! - - // assert - XCTAssertEqual(posts.count, 2) - } - - func test_transform_selectedPostInvoked_navigateToPost() { - // arrange - let select = PublishSubject() - let output = viewModel.transform(input: createInput(selection: select)) - let posts = createPosts() - allPostUseCase.posts_ReturnValue = Observable.just(posts) - - // act - output.posts.drive().disposed(by: disposeBag) - output.selectedPost.drive().disposed(by: disposeBag) - select.onNext(IndexPath(row: 1, section: 0)) - - // assert - XCTAssertTrue(postsNavigator.toPost_post_Called) - XCTAssertEqual(postsNavigator.toPost_post_ReceivedArguments, posts[1]) - } - - func test_transform_createPostInvoked_navigateToCreatePost() { - // arrange - let create = PublishSubject() - let output = viewModel.transform(input: createInput(createPostTrigger: create)) - let posts = createPosts() - allPostUseCase.posts_ReturnValue = Observable.just(posts) - - // act - output.posts.drive().disposed(by: disposeBag) - output.createPost.drive().disposed(by: disposeBag) - create.onNext() - - // assert - XCTAssertTrue(postsNavigator.toCreatePost_Called) - } - - private func createInput(trigger: Observable = Observable.just(), - createPostTrigger: Observable = Observable.never(), - selection: Observable = Observable.never()) - -> PostsViewModel.Input { - return PostsViewModel.Input( - trigger: trigger.asDriverOnErrorJustComplete(), - createPostTrigger: createPostTrigger.asDriverOnErrorJustComplete(), - selection: selection.asDriverOnErrorJustComplete()) - } - - private func createPosts() -> [Post] { - return [ - Post(body: "body 1", title: "title 1", uid: "uid 1", userId: "userId 1"), - Post(body: "body 2", title: "title 2", uid: "uid 2", userId: "userId 2") - ] - } -} diff --git a/CoreDataPlatform/CoreDataPlatform.h b/CoreDataPlatform/CoreDataPlatform.h deleted file mode 100644 index cd4adaa6..00000000 --- a/CoreDataPlatform/CoreDataPlatform.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// CoreDataPlatform.h -// CoreDataPlatform -// -// Created by sergdort on 18/02/2017. -// Copyright © 2017 sergdort. All rights reserved. -// - -#import - -//! Project version number for CoreDataPlatform. -FOUNDATION_EXPORT double CoreDataPlatformVersionNumber; - -//! Project version string for CoreDataPlatform. -FOUNDATION_EXPORT const unsigned char CoreDataPlatformVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/CoreDataPlatform/CoreDataStack/CoreDataStack.swift b/CoreDataPlatform/CoreDataStack/CoreDataStack.swift deleted file mode 100644 index efc86c55..00000000 --- a/CoreDataPlatform/CoreDataStack/CoreDataStack.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation -import CoreData - -final class CoreDataStack { - private let storeCoordinator: NSPersistentStoreCoordinator - let context: NSManagedObjectContext - - public init() { - let bundle = Bundle(for: CoreDataStack.self) - guard let url = bundle.url(forResource: "Model", withExtension: "momd"), - let model = NSManagedObjectModel(contentsOf: url) else { - fatalError() - } - self.storeCoordinator = NSPersistentStoreCoordinator(managedObjectModel: model) - self.context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - self.context.persistentStoreCoordinator = self.storeCoordinator - self.migrateStore() - } - - private func migrateStore() { - guard let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last else { - fatalError() - } - let storeUrl = url.appendingPathComponent("Model.sqlite") - do { - try storeCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, - configurationName: nil, - at: storeUrl, - options: nil) - } catch { - fatalError("Error migrating store: \(error)") - } - } -} diff --git a/CoreDataPlatform/Entities/CDAddress+CoreDataClass.swift b/CoreDataPlatform/Entities/CDAddress+CoreDataClass.swift deleted file mode 100644 index e7fc423e..00000000 --- a/CoreDataPlatform/Entities/CDAddress+CoreDataClass.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// CDAddress+CoreDataClass.swift -// -// -// Created by Andrey Yastrebov on 10.03.17. -// -// This file was automatically generated and should not be edited. -// - -import Foundation -import CoreData - - -final class CDAddress: NSManagedObject { - -} diff --git a/CoreDataPlatform/Entities/CDAddress+CoreDataProperties.swift b/CoreDataPlatform/Entities/CDAddress+CoreDataProperties.swift deleted file mode 100644 index 29a6e46c..00000000 --- a/CoreDataPlatform/Entities/CDAddress+CoreDataProperties.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// CDAddress+CoreDataProperties.swift -// -// -// Created by Andrey Yastrebov on 04.04.17. -// -// - -import Foundation -import CoreData - - -extension CDAddress { - - @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: "CDAddress") - } - - @NSManaged public var city: String? - @NSManaged public var street: String? - @NSManaged public var suite: String? - @NSManaged public var zipcode: String? - @NSManaged public var geo: CDLocation? - @NSManaged public var user: CDUser? - -} diff --git a/CoreDataPlatform/Entities/CDAddress+Ext.swift b/CoreDataPlatform/Entities/CDAddress+Ext.swift deleted file mode 100644 index 1469badd..00000000 --- a/CoreDataPlatform/Entities/CDAddress+Ext.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// CDAddress+Ext.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Foundation -import CoreData -import Domain -import QueryKit -import RxSwift - -extension CDAddress { - static var city: Attribute { return Attribute("city")} - static var street: Attribute { return Attribute("street")} - static var suite: Attribute { return Attribute("suite")} - static var zipcode: Attribute { return Attribute("zipcode")} - static var geo: Attribute { return Attribute("geo")} -} - -extension CDAddress: DomainConvertibleType { - func asDomain() -> Address { - return Address(city: city!, - geo: (geo?.asDomain())!, - street: street!, - suite: suite!, - zipcode: zipcode!) - } -} - -extension CDAddress: Persistable { - static var entityName: String { - return "CDAddress" - } - - static func synced(address: CDAddress, with geo: CDLocation?) -> CDAddress { - address.geo = geo - return address - } -} - -extension Address: CoreDataRepresentable { - internal var uid: String { - return "" - } - - typealias CoreDataType = CDAddress - - func sync(in context: NSManagedObjectContext) -> Observable { - let syncSelf = context.rx.sync(entity: self, update: update) - let syncGeo = geo.sync(in: context).map(Optional.init) - return Observable.zip(syncSelf, syncGeo, resultSelector: CDAddress.synced) - } - - func update(entity: CDAddress) { - entity.city = city - entity.street = street - entity.suite = suite - entity.zipcode = zipcode - } -} diff --git a/CoreDataPlatform/Entities/CDAlbum+CoreDataClass.swift b/CoreDataPlatform/Entities/CDAlbum+CoreDataClass.swift deleted file mode 100644 index 42a858d2..00000000 --- a/CoreDataPlatform/Entities/CDAlbum+CoreDataClass.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// CDAlbum+CoreDataClass.swift -// -// -// Created by Andrey Yastrebov on 10.03.17. -// -// This file was automatically generated and should not be edited. -// - -import Foundation -import CoreData - - -final class CDAlbum: NSManagedObject { - -} diff --git a/CoreDataPlatform/Entities/CDAlbum+CoreDataProperties.swift b/CoreDataPlatform/Entities/CDAlbum+CoreDataProperties.swift deleted file mode 100644 index 9a0a7566..00000000 --- a/CoreDataPlatform/Entities/CDAlbum+CoreDataProperties.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// CDAlbum+CoreDataProperties.swift -// -// -// Created by Andrey Yastrebov on 04.04.17. -// -// - -import Foundation -import CoreData - - -extension CDAlbum { - - @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: "CDAlbum") - } - - @NSManaged public var title: String? - @NSManaged public var uid: String? - @NSManaged public var userId: String? - -} diff --git a/CoreDataPlatform/Entities/CDAlbum+Ext.swift b/CoreDataPlatform/Entities/CDAlbum+Ext.swift deleted file mode 100644 index 9652206f..00000000 --- a/CoreDataPlatform/Entities/CDAlbum+Ext.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// CDAlbum+Ext.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Foundation -import CoreData -import Domain -import QueryKit -import RxSwift - -extension CDAlbum { - static var title: Attribute { return Attribute("title")} - static var userId: Attribute { return Attribute("userId")} - static var uid: Attribute { return Attribute("uid")} -} - -extension CDAlbum: DomainConvertibleType { - func asDomain() -> Album { - return Album(title: title!, - uid: uid!, - userId: userId!) - } -} - -extension CDAlbum: Persistable { - static var entityName: String { - return "CDAlbum" - } -} - -extension Album: CoreDataRepresentable { - typealias CoreDataType = CDAlbum - - func update(entity: CDAlbum) { - entity.uid = uid - entity.title = title - entity.userId = userId - } -} diff --git a/CoreDataPlatform/Entities/CDComment+CoreDataClass.swift b/CoreDataPlatform/Entities/CDComment+CoreDataClass.swift deleted file mode 100644 index 9e60a285..00000000 --- a/CoreDataPlatform/Entities/CDComment+CoreDataClass.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// CDComment+CoreDataClass.swift -// -// -// Created by Andrey Yastrebov on 10.03.17. -// -// This file was automatically generated and should not be edited. -// - -import Foundation -import CoreData - - -final class CDComment: NSManagedObject { - -} diff --git a/CoreDataPlatform/Entities/CDComment+CoreDataProperties.swift b/CoreDataPlatform/Entities/CDComment+CoreDataProperties.swift deleted file mode 100644 index 06114543..00000000 --- a/CoreDataPlatform/Entities/CDComment+CoreDataProperties.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// CDComment+CoreDataProperties.swift -// -// -// Created by Andrey Yastrebov on 04.04.17. -// -// - -import Foundation -import CoreData - - -extension CDComment { - - @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: "CDComment") - } - - @NSManaged public var body: String? - @NSManaged public var email: String? - @NSManaged public var name: String? - @NSManaged public var postId: String? - @NSManaged public var uid: String? - -} diff --git a/CoreDataPlatform/Entities/CDComment+Ext.swift b/CoreDataPlatform/Entities/CDComment+Ext.swift deleted file mode 100644 index 991a95b9..00000000 --- a/CoreDataPlatform/Entities/CDComment+Ext.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// CDComment+Ext.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Foundation -import CoreData -import Domain -import QueryKit -import RxSwift - -extension CDComment { - static var body: Attribute { return Attribute("body")} - static var email: Attribute { return Attribute("email")} - static var name: Attribute { return Attribute("name")} - static var postId: Attribute { return Attribute("postId")} - static var uid: Attribute { return Attribute("uid")} -} - -extension CDComment: DomainConvertibleType { - func asDomain() -> Comment { - return Comment(body: body!, - email: email!, - name: name!, - postId: postId!, - uid: uid!) - } -} - -extension CDComment: Persistable { - static var entityName: String { - return "CDComment" - } -} - -extension Comment: CoreDataRepresentable { - typealias CoreDataType = CDComment - - func update(entity: CDComment) { - entity.uid = uid - entity.name = name - entity.body = body - entity.email = email - entity.postId = postId - } -} diff --git a/CoreDataPlatform/Entities/CDCompany+CoreDataClass.swift b/CoreDataPlatform/Entities/CDCompany+CoreDataClass.swift deleted file mode 100644 index 47f96bc5..00000000 --- a/CoreDataPlatform/Entities/CDCompany+CoreDataClass.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// CDCompany+CoreDataClass.swift -// -// -// Created by Andrey Yastrebov on 10.03.17. -// -// This file was automatically generated and should not be edited. -// - -import Foundation -import CoreData - - -final class CDCompany: NSManagedObject { - -} diff --git a/CoreDataPlatform/Entities/CDCompany+CoreDataProperties.swift b/CoreDataPlatform/Entities/CDCompany+CoreDataProperties.swift deleted file mode 100644 index 845edf13..00000000 --- a/CoreDataPlatform/Entities/CDCompany+CoreDataProperties.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// CDCompany+CoreDataProperties.swift -// -// -// Created by Andrey Yastrebov on 04.04.17. -// -// - -import Foundation -import CoreData - - -extension CDCompany { - - @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: "CDCompany") - } - - @NSManaged public var bs: String? - @NSManaged public var catchPhrase: String? - @NSManaged public var name: String? - @NSManaged public var user: CDUser? - -} diff --git a/CoreDataPlatform/Entities/CDCompany+Ext.swift b/CoreDataPlatform/Entities/CDCompany+Ext.swift deleted file mode 100644 index 93962c93..00000000 --- a/CoreDataPlatform/Entities/CDCompany+Ext.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// CDCompany+Ext.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Foundation -import CoreData -import Domain -import QueryKit -import RxSwift - -extension CDCompany { - static var bs: Attribute { return Attribute("bs")} - static var catchPhrase: Attribute { return Attribute("catchPhrase")} - static var name: Attribute { return Attribute("name")} -} - -extension CDCompany: DomainConvertibleType { - func asDomain() -> Company { - return Company(bs: bs!, - catchPhrase: catchPhrase!, - name: name!) - } -} - -extension CDCompany: Persistable { - static var entityName: String { - return "CDCompany" - } -} - -extension Company: CoreDataRepresentable { - internal var uid: String { - return "" - } - - typealias CoreDataType = CDCompany - - func update(entity: CDCompany) { - entity.bs = bs - entity.name = name - entity.catchPhrase = catchPhrase - } -} diff --git a/CoreDataPlatform/Entities/CDLocation+CoreDataClass.swift b/CoreDataPlatform/Entities/CDLocation+CoreDataClass.swift deleted file mode 100644 index 6e123c5e..00000000 --- a/CoreDataPlatform/Entities/CDLocation+CoreDataClass.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// CDLocation+CoreDataClass.swift -// -// -// Created by Andrey Yastrebov on 10.03.17. -// -// This file was automatically generated and should not be edited. -// - -import Foundation -import CoreData - - -final class CDLocation: NSManagedObject { - -} diff --git a/CoreDataPlatform/Entities/CDLocation+CoreDataProperties.swift b/CoreDataPlatform/Entities/CDLocation+CoreDataProperties.swift deleted file mode 100644 index 3a1d036d..00000000 --- a/CoreDataPlatform/Entities/CDLocation+CoreDataProperties.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// CDLocation+CoreDataProperties.swift -// -// -// Created by Andrey Yastrebov on 04.04.17. -// -// - -import Foundation -import CoreData - - -extension CDLocation { - - @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: "CDLocation") - } - - @NSManaged public var latitude: Double - @NSManaged public var longitude: Double - @NSManaged public var address: CDAddress? - -} diff --git a/CoreDataPlatform/Entities/CDLocation+Ext.swift b/CoreDataPlatform/Entities/CDLocation+Ext.swift deleted file mode 100644 index f909f44b..00000000 --- a/CoreDataPlatform/Entities/CDLocation+Ext.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation -import CoreData -import Domain -import QueryKit - -extension CDLocation { - static var latitude: Attribute { return Attribute("latitude")} - static var longitude: Attribute { return Attribute("longitude")} - static var name: Attribute { return Attribute("name")} - static var post: Attribute { return Attribute("post")} -} - -extension CDLocation: DomainConvertibleType { - func asDomain() -> Location { - return Location(latitude: latitude, - longitude: longitude) - } -} - -extension CDLocation: Persistable { - static var entityName: String { - return "CDLocation" - } -} - -extension Location: CoreDataRepresentable { - internal var uid: String { - return ""; - } - - typealias CoreDataType = CDLocation - - func update(entity: CDLocation) { - entity.latitude = latitude - entity.longitude = longitude - } -} diff --git a/CoreDataPlatform/Entities/CDPhoto+CoreDataClass.swift b/CoreDataPlatform/Entities/CDPhoto+CoreDataClass.swift deleted file mode 100644 index a0401a84..00000000 --- a/CoreDataPlatform/Entities/CDPhoto+CoreDataClass.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// CDPhoto+CoreDataClass.swift -// -// -// Created by Andrey Yastrebov on 10.03.17. -// -// This file was automatically generated and should not be edited. -// - -import Foundation -import CoreData - - -final class CDPhoto: NSManagedObject { - -} diff --git a/CoreDataPlatform/Entities/CDPhoto+CoreDataProperties.swift b/CoreDataPlatform/Entities/CDPhoto+CoreDataProperties.swift deleted file mode 100644 index fbbd4848..00000000 --- a/CoreDataPlatform/Entities/CDPhoto+CoreDataProperties.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// CDPhoto+CoreDataProperties.swift -// -// -// Created by Andrey Yastrebov on 04.04.17. -// -// - -import Foundation -import CoreData - - -extension CDPhoto { - - @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: "CDPhoto") - } - - @NSManaged public var albumId: String? - @NSManaged public var thumbnailUrl: String? - @NSManaged public var title: String? - @NSManaged public var uid: String? - @NSManaged public var url: String? - -} diff --git a/CoreDataPlatform/Entities/CDPhoto+Ext.swift b/CoreDataPlatform/Entities/CDPhoto+Ext.swift deleted file mode 100644 index fb2a874a..00000000 --- a/CoreDataPlatform/Entities/CDPhoto+Ext.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// CDPhoto+Ext.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Foundation -import CoreData -import Domain -import QueryKit -import RxSwift - -extension CDPhoto { - static var title: Attribute { return Attribute("title")} - static var thumbnailUrl: Attribute { return Attribute("thumbnailUrl")} - static var url: Attribute { return Attribute("url")} - static var albumId: Attribute { return Attribute("albumId")} - static var uid: Attribute { return Attribute("uid")} -} - -extension CDPhoto: DomainConvertibleType { - func asDomain() -> Photo { - return Photo(albumId: albumId!, - thumbnailUrl: thumbnailUrl!, - title: title!, - uid: uid!, - url: url!) - } -} - -extension CDPhoto: Persistable { - static var entityName: String { - return "CDPhoto" - } -} - -extension Photo: CoreDataRepresentable { - typealias CoreDataType = CDPhoto - - func update(entity: CDPhoto) { - entity.uid = uid - entity.title = title - entity.url = url - entity.thumbnailUrl = thumbnailUrl - entity.albumId = albumId - } -} diff --git a/CoreDataPlatform/Entities/CDPost+CoreDataClass.swift b/CoreDataPlatform/Entities/CDPost+CoreDataClass.swift deleted file mode 100644 index fcced10f..00000000 --- a/CoreDataPlatform/Entities/CDPost+CoreDataClass.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// CDPost+CoreDataClass.swift -// -// -// Created by Andrey Yastrebov on 10.03.17. -// -// This file was automatically generated and should not be edited. -// - -import Foundation -import CoreData - - -final class CDPost: NSManagedObject { - -} diff --git a/CoreDataPlatform/Entities/CDPost+CoreDataProperties.swift b/CoreDataPlatform/Entities/CDPost+CoreDataProperties.swift deleted file mode 100644 index 309565cb..00000000 --- a/CoreDataPlatform/Entities/CDPost+CoreDataProperties.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// CDPost+CoreDataProperties.swift -// -// -// Created by Andrey Yastrebov on 04.04.17. -// -// - -import Foundation -import CoreData - - -extension CDPost { - - @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: "CDPost") - } - - @NSManaged public var body: String? - @NSManaged public var title: String? - @NSManaged public var uid: String? - @NSManaged public var userId: String? - @NSManaged public var createdAt: String? -} diff --git a/CoreDataPlatform/Entities/CDPost+Ext.swift b/CoreDataPlatform/Entities/CDPost+Ext.swift deleted file mode 100644 index 587d6b64..00000000 --- a/CoreDataPlatform/Entities/CDPost+Ext.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// CDPost+CoreDataClass.swift -// NetworkAndSecurity -// -// Created by sergdort on 07/01/2017. -// Copyright © 2017 sergdort. All rights reserved. -// This file was automatically generated and should not be edited. -// - -import Foundation -import CoreData -import Domain -import QueryKit -import RxSwift - -extension CDPost { - static var title: Attribute { return Attribute("title")} - static var body: Attribute { return Attribute("body")} - static var userId: Attribute { return Attribute("userId")} - static var uid: Attribute { return Attribute("uid")} - static var createdAt: Attribute { return Attribute("createdAt")} -} - -extension CDPost: DomainConvertibleType { - func asDomain() -> Post { - return Post(body: body!, - title: title!, - uid: uid!, - userId: userId!, - createdAt: createdAt!) - } -} - -extension CDPost: Persistable { - static var entityName: String { - return "CDPost" - } -} - -extension Post: CoreDataRepresentable { - typealias CoreDataType = CDPost - - func update(entity: CDPost) { - entity.uid = uid - entity.title = title - entity.body = body - entity.userId = userId - entity.createdAt = createdAt - } -} diff --git a/CoreDataPlatform/Entities/CDTodo+CoreDataClass.swift b/CoreDataPlatform/Entities/CDTodo+CoreDataClass.swift deleted file mode 100644 index 9d2de530..00000000 --- a/CoreDataPlatform/Entities/CDTodo+CoreDataClass.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// CDTodo+CoreDataClass.swift -// -// -// Created by Andrey Yastrebov on 10.03.17. -// -// This file was automatically generated and should not be edited. -// - -import Foundation -import CoreData - - -final class CDTodo: NSManagedObject { - -} diff --git a/CoreDataPlatform/Entities/CDTodo+CoreDataProperties.swift b/CoreDataPlatform/Entities/CDTodo+CoreDataProperties.swift deleted file mode 100644 index a39ffabc..00000000 --- a/CoreDataPlatform/Entities/CDTodo+CoreDataProperties.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// CDTodo+CoreDataProperties.swift -// -// -// Created by Andrey Yastrebov on 04.04.17. -// -// - -import Foundation -import CoreData - - -extension CDTodo { - - @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: "CDTodo") - } - - @NSManaged public var completed: Bool - @NSManaged public var title: String? - @NSManaged public var uid: String? - @NSManaged public var userId: String? - -} diff --git a/CoreDataPlatform/Entities/CDTodo+Ext.swift b/CoreDataPlatform/Entities/CDTodo+Ext.swift deleted file mode 100644 index dfbf8a0d..00000000 --- a/CoreDataPlatform/Entities/CDTodo+Ext.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// CDTodo+Ext.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Foundation -import CoreData -import Domain -import QueryKit -import RxSwift - -extension CDTodo { - static var title: Attribute { return Attribute("title")} - static var completed: Attribute { return Attribute("completed")} - static var userId: Attribute { return Attribute("userId")} - static var uid: Attribute { return Attribute("uid")} -} - -extension CDTodo: DomainConvertibleType { - func asDomain() -> Todo { - return Todo(completed: completed, - title: title!, - uid: uid!, - userId: userId!) - } -} - -extension CDTodo: Persistable { - static var entityName: String { - return "CDTodo" - } -} - -extension Todo: CoreDataRepresentable { - typealias CoreDataType = CDTodo - - func update(entity: CDTodo) { - entity.uid = uid - entity.completed = completed - entity.title = title - entity.userId = userId - } -} diff --git a/CoreDataPlatform/Entities/CDUser+CoreDataClass.swift b/CoreDataPlatform/Entities/CDUser+CoreDataClass.swift deleted file mode 100644 index d6839bca..00000000 --- a/CoreDataPlatform/Entities/CDUser+CoreDataClass.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// CDUser+CoreDataClass.swift -// -// -// Created by Andrey Yastrebov on 10.03.17. -// -// This file was automatically generated and should not be edited. -// - -import Foundation -import CoreData - - -final class CDUser: NSManagedObject { - -} diff --git a/CoreDataPlatform/Entities/CDUser+CoreDataProperties.swift b/CoreDataPlatform/Entities/CDUser+CoreDataProperties.swift deleted file mode 100644 index 79b3dd28..00000000 --- a/CoreDataPlatform/Entities/CDUser+CoreDataProperties.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// CDUser+CoreDataProperties.swift -// -// -// Created by Andrey Yastrebov on 04.04.17. -// -// - -import Foundation -import CoreData - - -extension CDUser { - - @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: "CDUser") - } - - @NSManaged public var email: String? - @NSManaged public var name: String? - @NSManaged public var phone: String? - @NSManaged public var uid: String? - @NSManaged public var username: String? - @NSManaged public var website: String? - @NSManaged public var address: CDAddress? - @NSManaged public var company: CDCompany? - -} diff --git a/CoreDataPlatform/Entities/CDUser+Ext.swift b/CoreDataPlatform/Entities/CDUser+Ext.swift deleted file mode 100644 index ff959668..00000000 --- a/CoreDataPlatform/Entities/CDUser+Ext.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// CDUser+Ext.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Foundation -import CoreData -import Domain -import QueryKit -import RxSwift - -extension CDUser { - static var website: Attribute { return Attribute("website")} - static var email: Attribute { return Attribute("email")} - static var name: Attribute { return Attribute("name")} - static var phone: Attribute { return Attribute("phone")} - static var username: Attribute { return Attribute("username")} - static var uid: Attribute { return Attribute("uid")} - static var address: Attribute { return Attribute("address")} - static var company: Attribute { return Attribute("company")} -} - -extension CDUser: DomainConvertibleType { - func asDomain() -> User { - return User(address: address!.asDomain(), - company: company!.asDomain(), - email: email!, - name: name!, - phone: phone!, - uid: uid!, - username: username!, - website: website!) - } -} - -extension CDUser: Persistable { - static var entityName: String { - return "CDUser" - } - - static func synced(user: CDUser, with address: CDAddress?, company: CDCompany?) -> CDUser { - user.address = address - user.company = company - return user - } -} - -extension User: CoreDataRepresentable { - typealias CoreDataType = CDUser - - func sync(in context: NSManagedObjectContext) -> Observable { - let syncSelf = context.rx.sync(entity: self, update: update) - let syncAddress = address.sync(in: context).map(Optional.init) - let syncCompany = company.sync(in: context).map(Optional.init) - return Observable.zip(syncSelf, syncAddress, syncCompany, resultSelector: CDUser.synced) - } - - func update(entity: CDUser) { - entity.uid = uid - entity.website = website - entity.email = email - entity.phone = phone - entity.username = username - } -} diff --git a/CoreDataPlatform/Entities/Convertion/DomainConvertibleType.swift b/CoreDataPlatform/Entities/Convertion/DomainConvertibleType.swift deleted file mode 100644 index c866ef76..00000000 --- a/CoreDataPlatform/Entities/Convertion/DomainConvertibleType.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation -import CoreData -import RxSwift -import QueryKit - -protocol DomainConvertibleType { - associatedtype DomainType - - func asDomain() -> DomainType -} - - - -protocol CoreDataRepresentable { - associatedtype CoreDataType: Persistable - - var uid: String {get} - - func update(entity: CoreDataType) -} - -extension CoreDataRepresentable { - func sync(in context: NSManagedObjectContext) -> Observable { - return context.rx.sync(entity: self, update: update) - } -} diff --git a/CoreDataPlatform/Entities/Convertion/Persistable.swift b/CoreDataPlatform/Entities/Convertion/Persistable.swift deleted file mode 100644 index f0fc135e..00000000 --- a/CoreDataPlatform/Entities/Convertion/Persistable.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import CoreData -import RxSwift -import QueryKit - -protocol Persistable: NSFetchRequestResult, DomainConvertibleType { - static var entityName: String {get} - static func fetchRequest() -> NSFetchRequest -} - -extension Persistable { - static var primaryAttribute: Attribute { - return Attribute("uid") - } -} diff --git a/CoreDataPlatform/Extensions/NSManagedObjectContext+Ext.swift b/CoreDataPlatform/Extensions/NSManagedObjectContext+Ext.swift deleted file mode 100644 index 76ab4627..00000000 --- a/CoreDataPlatform/Extensions/NSManagedObjectContext+Ext.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation -import CoreData - -extension NSManagedObjectContext { - func create() -> T { - guard let entity = NSEntityDescription.insertNewObject(forEntityName: String(describing: T.self), - into: self) as? T else { - fatalError() - } - return entity - } -} diff --git a/CoreDataPlatform/Extensions/Observable+Ext.swift b/CoreDataPlatform/Extensions/Observable+Ext.swift deleted file mode 100644 index 0c67daeb..00000000 --- a/CoreDataPlatform/Extensions/Observable+Ext.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation -import RxSwift - -extension ObservableType { - func mapToVoid() -> Observable { - return map { _ in } - } -} - - -extension Observable where Element: Sequence, Element.Iterator.Element: DomainConvertibleType { - typealias DomainType = Element.Iterator.Element.DomainType - - func mapToDomain() -> Observable<[DomainType]> { - return map { sequence -> [DomainType] in - return sequence.mapToDomain() - } - } -} - -extension Sequence where Iterator.Element: DomainConvertibleType { - typealias Element = Iterator.Element - func mapToDomain() -> [Element.DomainType] { - return map { - return $0.asDomain() - } - } -} diff --git a/CoreDataPlatform/Info.plist b/CoreDataPlatform/Info.plist deleted file mode 100644 index fbe1e6b3..00000000 --- a/CoreDataPlatform/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - - - diff --git a/CoreDataPlatform/Model.xcdatamodeld/Model.xcdatamodel/contents b/CoreDataPlatform/Model.xcdatamodeld/Model.xcdatamodel/contents deleted file mode 100644 index 1d2cd624..00000000 --- a/CoreDataPlatform/Model.xcdatamodeld/Model.xcdatamodel/contents +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/CoreDataPlatform/Repository/Repository.swift b/CoreDataPlatform/Repository/Repository.swift deleted file mode 100644 index 3c90aea0..00000000 --- a/CoreDataPlatform/Repository/Repository.swift +++ /dev/null @@ -1,46 +0,0 @@ -import Foundation -import CoreData -import RxSwift -import QueryKit - -protocol AbstractRepository { - associatedtype T - func query(with predicate: NSPredicate?, - sortDescriptors: [NSSortDescriptor]?) -> Observable<[T]> - func save(entity: T) -> Observable - func delete(entity: T) -> Observable -} - -final class Repository: AbstractRepository where T == T.CoreDataType.DomainType { - private let context: NSManagedObjectContext - private let scheduler: ContextScheduler - - init(context: NSManagedObjectContext) { - self.context = context - self.scheduler = ContextScheduler(context: context) - } - - func query(with predicate: NSPredicate? = nil, - sortDescriptors: [NSSortDescriptor]? = nil) -> Observable<[T]> { - let request = T.CoreDataType.fetchRequest() - request.predicate = predicate - request.sortDescriptors = sortDescriptors - return context.rx.entities(fetchRequest: request) - .mapToDomain() - .subscribeOn(scheduler) - } - - func save(entity: T) -> Observable { - return entity.sync(in: context) - .mapToVoid() - .flatMapLatest(context.rx.save) - .subscribeOn(scheduler) - } - - func delete(entity: T) -> Observable { - return entity.sync(in: context) - .map({$0 as! NSManagedObject}) - .flatMapLatest(context.rx.delete) - } - -} diff --git a/CoreDataPlatform/RxCoreData/ContextScheduler.swift b/CoreDataPlatform/RxCoreData/ContextScheduler.swift deleted file mode 100644 index 5113dc86..00000000 --- a/CoreDataPlatform/RxCoreData/ContextScheduler.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation -import CoreData -import RxSwift - -final class ContextScheduler: ImmediateSchedulerType { - private let context: NSManagedObjectContext - - init(context: NSManagedObjectContext) { - self.context = context - } - - func schedule(_ state: StateType, action: @escaping (StateType) -> Disposable) -> Disposable { - - let disposable = SingleAssignmentDisposable() - - context.perform { - if disposable.isDisposed { - return - } - disposable.setDisposable(action(state)) - } - - return disposable - } -} diff --git a/CoreDataPlatform/RxCoreData/FetchedResultsControllerEntityObserver.swift b/CoreDataPlatform/RxCoreData/FetchedResultsControllerEntityObserver.swift deleted file mode 100644 index 2106c6c4..00000000 --- a/CoreDataPlatform/RxCoreData/FetchedResultsControllerEntityObserver.swift +++ /dev/null @@ -1,55 +0,0 @@ -import Foundation -import CoreData -import RxSwift - -final class FetchedResultsControllerEntityObserver : NSObject, NSFetchedResultsControllerDelegate { - - typealias Observer = AnyObserver<[T]> - - fileprivate let observer: Observer - fileprivate let disposeBag = DisposeBag() - fileprivate let frc: NSFetchedResultsController - - - init(observer: Observer, fetchRequest: NSFetchRequest, managedObjectContext context: NSManagedObjectContext, sectionNameKeyPath: String?, cacheName: String?) { - self.observer = observer - - - self.frc = NSFetchedResultsController(fetchRequest: fetchRequest, - managedObjectContext: context, - sectionNameKeyPath: sectionNameKeyPath, - cacheName: cacheName) - super.init() - - context.perform { - self.frc.delegate = self - - do { - try self.frc.performFetch() - } catch let e { - observer.on(.error(e)) - } - - self.sendNextElement() - } - } - - fileprivate func sendNextElement() { - self.frc.managedObjectContext.perform { - let entities = self.frc.fetchedObjects ?? [] - self.observer.on(.next(entities)) - } - } - - public func controllerDidChangeContent(_ controller: NSFetchedResultsController) { - sendNextElement() - } -} - -extension FetchedResultsControllerEntityObserver : Disposable { - - public func dispose() { - frc.delegate = nil - } - -} diff --git a/CoreDataPlatform/RxCoreData/NSManagedObjectContext+Rx.swift b/CoreDataPlatform/RxCoreData/NSManagedObjectContext+Rx.swift deleted file mode 100644 index 78a32a1e..00000000 --- a/CoreDataPlatform/RxCoreData/NSManagedObjectContext+Rx.swift +++ /dev/null @@ -1,75 +0,0 @@ -import Foundation -import CoreData -import RxSwift -import QueryKit - -extension Reactive where Base: NSManagedObjectContext { - - /** - Executes a fetch request and returns the fetched objects as an `Observable` array of `NSManagedObjects`. - - parameter fetchRequest: an instance of `NSFetchRequest` to describe the search criteria used to retrieve data from a persistent store - - parameter sectionNameKeyPath: the key path on the fetched objects used to determine the section they belong to; defaults to `nil` - - parameter cacheName: the name of the file used to cache section information; defaults to `nil` - - returns: An `Observable` array of `NSManagedObjects` objects that can be bound to a table view. - */ - func entities(fetchRequest: NSFetchRequest, - sectionNameKeyPath: String? = nil, - cacheName: String? = nil) -> Observable<[T]> { - return Observable.create { observer in - - let observerAdapter = FetchedResultsControllerEntityObserver(observer: observer, fetchRequest: fetchRequest, managedObjectContext: self.base, sectionNameKeyPath: sectionNameKeyPath, cacheName: cacheName) - - return Disposables.create { - observerAdapter.dispose() - } - } - } - - func save() -> Observable { - return Observable.create { observer in - do { - try self.base.save() - observer.onNext(()) - } catch { - observer.onError(error) - } - return Disposables.create() - } - } - - func delete(entity: T) -> Observable { - return Observable.create { observer in - self.base.delete(entity) - observer.onNext(()) - return Disposables.create() - }.flatMapLatest { - self.save() - } - } - - func first(ofType: T.Type = T.self, with predicate: NSPredicate) -> Observable { - return Observable.deferred { - let entityName = String(describing: T.self) - let request = NSFetchRequest(entityName: entityName) - request.predicate = predicate - do { - let result = try self.base.fetch(request).first - return Observable.just(result) - } catch { - return Observable.error(error) - } - } - } - - func sync(entity: C, - update: @escaping (P) -> Void) -> Observable

where C.CoreDataType == P { - let predicate: NSPredicate = P.primaryAttribute == entity.uid - return first(ofType: P.self, with: predicate) - .flatMap { obj -> Observable

in - let object = obj ?? self.base.create() - update(object) - return Observable.just(object) - } - } -} - diff --git a/CoreDataPlatform/UseCases/PostsUseCase.swift b/CoreDataPlatform/UseCases/PostsUseCase.swift deleted file mode 100644 index 354b2fae..00000000 --- a/CoreDataPlatform/UseCases/PostsUseCase.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation -import Domain -import RxSwift - -final class PostsUseCase: Domain.PostsUseCase where Repository: AbstractRepository, Repository.T == Post { - - private let repository: Repository - - init(repository: Repository) { - self.repository = repository - } - - func posts() -> Observable<[Post]> { - return repository.query(with: nil, sortDescriptors: [Post.CoreDataType.createdAt.descending()]) - } - - func save(post: Post) -> Observable { - return repository.save(entity: post) - } - - func delete(post: Post) -> Observable { - return repository.delete(entity: post) - } -} diff --git a/CoreDataPlatform/UseCases/UseCaseProvider.swift b/CoreDataPlatform/UseCases/UseCaseProvider.swift deleted file mode 100644 index 89ed16e4..00000000 --- a/CoreDataPlatform/UseCases/UseCaseProvider.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import Domain - -public final class UseCaseProvider: Domain.UseCaseProvider { - private let coreDataStack = CoreDataStack() - private let postRepository: Repository - - public init() { - postRepository = Repository(context: coreDataStack.context) - } - - public func makePostsUseCase() -> Domain.PostsUseCase { - return PostsUseCase(repository: postRepository) - } -} diff --git a/CoreDataPlatformTests/CoreDataPlatformTests.swift b/CoreDataPlatformTests/CoreDataPlatformTests.swift deleted file mode 100644 index 814f33e8..00000000 --- a/CoreDataPlatformTests/CoreDataPlatformTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// CoreDataPlatformTests.swift -// CoreDataPlatformTests -// -// Created by sergdort on 18/02/2017. -// Copyright © 2017 sergdort. All rights reserved. -// - -import XCTest -@testable import CoreDataPlatform - -class CoreDataPlatformTests: XCTestCase { - - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testPerformanceExample() { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/CoreDataPlatformTests/Info.plist b/CoreDataPlatformTests/Info.plist deleted file mode 100644 index 6c6c23c4..00000000 --- a/CoreDataPlatformTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/Domain/Domain.h b/Domain/Domain.h deleted file mode 100644 index 5c82e384..00000000 --- a/Domain/Domain.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// Domain.h -// Domain -// -// Created by sergdort on 18/02/2017. -// Copyright © 2017 sergdort. All rights reserved. -// - -#import - -//! Project version number for Domain. -FOUNDATION_EXPORT double DomainVersionNumber; - -//! Project version string for Domain. -FOUNDATION_EXPORT const unsigned char DomainVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/Domain/Entries/Address.swift b/Domain/Entries/Address.swift deleted file mode 100644 index 66a0235a..00000000 --- a/Domain/Entries/Address.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// Address.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Foundation - -public struct Address: Codable { - - public let city: String - public let geo: Location - public let street: String - public let suite: String - public let zipcode: String - - public init(city: String, - geo: Location, - street: String, - suite: String, - zipcode: String) { - self.city = city - self.geo = geo - self.street = street - self.suite = suite - self.zipcode = zipcode - } -} - -extension Address: Equatable { - public static func == (lhs: Address, rhs: Address) -> Bool { - return lhs.city == rhs.city && - lhs.geo == rhs.geo && - lhs.street == rhs.street && - lhs.suite == rhs.suite && - lhs.zipcode == rhs.zipcode - } -} diff --git a/Domain/Entries/Album.swift b/Domain/Entries/Album.swift deleted file mode 100644 index 190826aa..00000000 --- a/Domain/Entries/Album.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// Album.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Foundation - -public struct Album: Codable { - public let title: String - public let uid: String - public let userId: String - - public init(title: String, - uid: String, - userId: String) { - self.title = title - self.uid = uid - self.userId = userId - } -} - -extension Album: Equatable { - public static func == (lhs: Album, rhs: Album) -> Bool { - return lhs.uid == rhs.uid && - lhs.title == rhs.title && - lhs.userId == rhs.userId - } -} diff --git a/Domain/Entries/Comment.swift b/Domain/Entries/Comment.swift deleted file mode 100644 index b141d9b9..00000000 --- a/Domain/Entries/Comment.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// Comment.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Foundation - -public struct Comment: Codable { - public let body: String - public let email: String - public let name: String - public let postId: String - public let uid: String - - public init(body: String, - email: String, - name: String, - postId: String, - uid: String) { - self.body = body - self.email = email - self.name = name - self.postId = postId - self.uid = uid - } -} - -extension Comment: Equatable { - public static func == (lhs: Comment, rhs: Comment) -> Bool { - return lhs.uid == rhs.uid && - lhs.name == rhs.name && - lhs.body == rhs.body && - lhs.postId == rhs.postId && - lhs.email == rhs.email - } -} diff --git a/Domain/Entries/Company.swift b/Domain/Entries/Company.swift deleted file mode 100644 index a24d8bde..00000000 --- a/Domain/Entries/Company.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// Company.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Foundation - -public struct Company: Codable { - public let bs: String - public let catchPhrase: String - public let name: String - - public init(bs: String, - catchPhrase: String, - name: String) { - self.bs = bs - self.catchPhrase = catchPhrase - self.name = name - } -} - -extension Company: Equatable { - public static func == (lhs: Company, rhs: Company) -> Bool { - return lhs.bs == rhs.bs && - lhs.catchPhrase == rhs.catchPhrase && - lhs.name == rhs.name - } -} diff --git a/Domain/Entries/Location.swift b/Domain/Entries/Location.swift deleted file mode 100644 index 17295538..00000000 --- a/Domain/Entries/Location.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation - -public struct Location: Codable { - public let latitude: Double - public let longitude: Double - - public init(latitude: Double, - longitude: Double) { - self.latitude = latitude - self.longitude = longitude - } -} - -extension Location: Equatable { - public static func == (lhs: Location, rhs: Location) -> Bool { - return lhs.latitude == rhs.latitude && - lhs.longitude == rhs.longitude - } -} diff --git a/Domain/Entries/Photo.swift b/Domain/Entries/Photo.swift deleted file mode 100644 index 908a6536..00000000 --- a/Domain/Entries/Photo.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// Photo.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Foundation - -public struct Photo: Codable { - public let albumId: String - public let thumbnailUrl: String - public let title: String - public let uid: String - public let url: String - - public init(albumId: String, - thumbnailUrl: String, - title: String, - uid: String, - url: String) { - self.albumId = albumId - self.thumbnailUrl = thumbnailUrl - self.title = title - self.uid = uid - self.url = url - } -} - -extension Photo: Equatable { - public static func == (lhs: Photo, rhs: Photo) -> Bool { - return lhs.uid == rhs.uid && - lhs.title == rhs.title && - lhs.albumId == rhs.albumId && - lhs.url == rhs.url && - lhs.thumbnailUrl == rhs.thumbnailUrl - } -} diff --git a/Domain/Entries/Post.swift b/Domain/Entries/Post.swift deleted file mode 100644 index f131ac25..00000000 --- a/Domain/Entries/Post.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Foundation - -public struct Post: Codable { - public let body: String - public let title: String - public let uid: String - public let userId: String - public let createdAt: String - - public init(body: String, - title: String, - uid: String, - userId: String, - createdAt: String) { - self.body = body - self.title = title - self.uid = uid - self.userId = userId - self.createdAt = createdAt - } - - public init(body: String, title: String) { - self.init(body: body, title: title, uid: NSUUID().uuidString, userId: "5", createdAt: String(round(Date().timeIntervalSince1970 * 1000))) - } - - private enum CodingKeys: String, CodingKey { - case body - case title - case uid - case userId - case createdAt - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - body = try container.decode(String.self, forKey: .body) - title = try container.decode(String.self, forKey: .title) - - if let createdAt = try container.decodeIfPresent(Int.self, forKey: .createdAt) { - self.createdAt = "\(createdAt)" - } else { - createdAt = try container.decodeIfPresent(String.self, forKey: .createdAt) ?? "" - } - - if let userId = try container.decodeIfPresent(Int.self, forKey: .userId) { - self.userId = "\(userId)" - } else { - userId = try container.decode(String.self, forKey: .userId) - } - - if let uid = try container.decodeIfPresent(Int.self, forKey: .uid) { - self.uid = "\(uid)" - } else { - uid = try container.decodeIfPresent(String.self, forKey: .uid) ?? "" - } - } -} - -extension Post: Equatable { - public static func == (lhs: Post, rhs: Post) -> Bool { - return lhs.uid == rhs.uid && - lhs.title == rhs.title && - lhs.body == rhs.body && - lhs.userId == rhs.userId && - lhs.createdAt == rhs.createdAt - } -} diff --git a/Domain/Entries/Todo.swift b/Domain/Entries/Todo.swift deleted file mode 100644 index c9e35aa1..00000000 --- a/Domain/Entries/Todo.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// Todo.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Foundation - -public struct Todo: Decodable { - public let completed: Bool - public let title: String - public let uid: String - public let userId: String - - public init(completed: Bool, - title: String, - uid: String, - userId: String) { - self.completed = completed - self.title = title - self.uid = uid - self.userId = userId - } - - private enum CodingKeys: String, CodingKey { - case completed - case title - case uid - case userId - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - completed = try container.decode(Bool.self, forKey: .completed) - title = try container.decode(String.self, forKey: .title) - - if let userId = try container.decodeIfPresent(Int.self, forKey: .userId) { - self.userId = "\(userId)" - } else { - userId = try container.decode(String.self, forKey: .userId) - } - - if let uid = try container.decodeIfPresent(Int.self, forKey: .uid) { - self.uid = "\(uid)" - } else { - uid = try container.decodeIfPresent(String.self, forKey: .uid) ?? "" - } - } -} - -extension Todo: Equatable { - public static func == (lhs: Todo, rhs: Todo) -> Bool { - return lhs.uid == rhs.uid && - lhs.title == rhs.title && - lhs.completed == rhs.completed && - lhs.userId == rhs.userId - } -} diff --git a/Domain/Entries/User.swift b/Domain/Entries/User.swift deleted file mode 100644 index d646a525..00000000 --- a/Domain/Entries/User.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// User.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Foundation - -public struct User: Codable { - - public let address: Address - public let company: Company - public let email: String - public let name: String - public let phone: String - public let uid: String - public let username: String - public let website: String - - public init(address: Address, - company: Company, - email: String, - name: String, - phone: String, - uid: String, - username: String, - website: String) { - self.address = address - self.company = company - self.email = email - self.name = name - self.phone = phone - self.uid = uid - self.username = username - self.website = website - } - - private enum CodingKeys: String, CodingKey { - case address - case company - case email - case name - case phone - case uid - case username - case website - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - address = try container.decode(Address.self, forKey: .address) - company = try container.decode(Company.self, forKey: .company) - email = try container.decode(String.self, forKey: .email) - name = try container.decode(String.self, forKey: .name) - phone = try container.decode(String.self, forKey: .phone) - username = try container.decode(String.self, forKey: .username) - website = try container.decode(String.self, forKey: .website) - - if let uid = try container.decodeIfPresent(Int.self, forKey: .uid) { - self.uid = "\(uid)" - } else { - uid = try container.decodeIfPresent(String.self, forKey: .uid) ?? "" - } - } -} - -extension User: Equatable { - public static func == (lhs: User, rhs: User) -> Bool { - return lhs.uid == rhs.uid && - lhs.address == rhs.address && - lhs.company == rhs.company && - lhs.email == rhs.email && - lhs.name == rhs.name && - lhs.phone == rhs.phone && - lhs.username == rhs.username && - lhs.website == rhs.website - } -} diff --git a/Domain/Info.plist b/Domain/Info.plist deleted file mode 100644 index fbe1e6b3..00000000 --- a/Domain/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - - - diff --git a/Domain/UseCases/PostsUseCase.swift b/Domain/UseCases/PostsUseCase.swift deleted file mode 100644 index 12051264..00000000 --- a/Domain/UseCases/PostsUseCase.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation -import RxSwift - -public protocol PostsUseCase { - func posts() -> Observable<[Post]> - func save(post: Post) -> Observable - func delete(post: Post) -> Observable -} diff --git a/Domain/UseCases/UseCaseProvider.swift b/Domain/UseCases/UseCaseProvider.swift deleted file mode 100644 index cf95ad0a..00000000 --- a/Domain/UseCases/UseCaseProvider.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// UseCaseProvider.swift -// CleanArchitectureRxSwift -// -// Created by sergdort on 18/02/2017. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Foundation - -public protocol UseCaseProvider { - - func makePostsUseCase() -> PostsUseCase -} diff --git a/DomainTests/DomainTests.swift b/DomainTests/DomainTests.swift deleted file mode 100644 index 96ac37e7..00000000 --- a/DomainTests/DomainTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// DomainTests.swift -// DomainTests -// -// Created by sergdort on 18/02/2017. -// Copyright © 2017 sergdort. All rights reserved. -// - -import XCTest -@testable import Domain - -class DomainTests: XCTestCase { - - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testPerformanceExample() { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/DomainTests/Info.plist b/DomainTests/Info.plist deleted file mode 100644 index 6c6c23c4..00000000 --- a/DomainTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 5d25f497..00000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2017 Sergey Shulga - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/NetworkPlatform/API/AlbumsNetwork.swift b/NetworkPlatform/API/AlbumsNetwork.swift deleted file mode 100644 index 8d3e2611..00000000 --- a/NetworkPlatform/API/AlbumsNetwork.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// AlbumsNetwork.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 16.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Domain -import RxSwift - -public final class AlbumsNetwork { - private let network: Network - - init(network: Network) { - self.network = network - } - - public func fetchAlbums() -> Observable<[Album]> { - return network.getItems("albums") - } - - public func fetchAlbum(albumId: String) -> Observable { - return network.getItem("albums", itemId: albumId) - } -} diff --git a/NetworkPlatform/API/CommentsNetwork.swift b/NetworkPlatform/API/CommentsNetwork.swift deleted file mode 100644 index c5cd9cca..00000000 --- a/NetworkPlatform/API/CommentsNetwork.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// CommentsNetwork.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 16.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Domain -import RxSwift - -public final class CommentsNetwork { - private let network: Network - - init(network: Network) { - self.network = network - } - - public func fetchComments() -> Observable<[Comment]> { - return network.getItems("comments") - } - - public func fetchComment(commentId: String) -> Observable { - return network.getItem("comments", itemId: commentId) - } -} diff --git a/NetworkPlatform/API/PhotosNetwork.swift b/NetworkPlatform/API/PhotosNetwork.swift deleted file mode 100644 index f798ed2f..00000000 --- a/NetworkPlatform/API/PhotosNetwork.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// PhotosNetwork.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 16.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Domain -import RxSwift - -public final class PhotosNetwork { - private let network: Network - - init(network: Network) { - self.network = network - } - - public func fetchPhotos() -> Observable<[Photo]> { - return network.getItems("photos") - } - - public func fetchPhoto(photoId: String) -> Observable { - return network.getItem("photos", itemId: photoId) - } -} diff --git a/NetworkPlatform/API/PostsNetwork.swift b/NetworkPlatform/API/PostsNetwork.swift deleted file mode 100644 index d3c583c5..00000000 --- a/NetworkPlatform/API/PostsNetwork.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// PostsNetwork.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Domain -import RxSwift - -public final class PostsNetwork { - private let network: Network - - init(network: Network) { - self.network = network - } - - public func fetchPosts() -> Observable<[Post]> { - return network.getItems("posts") - } - - public func fetchPost(postId: String) -> Observable { - return network.getItem("posts", itemId: postId) - } - - public func createPost(post: Post) -> Observable { - return network.postItem("posts", parameters: post.toJSON()) - } - - public func deletePost(postId: String) -> Observable { - return network.deleteItem("posts", itemId: postId) - } -} diff --git a/NetworkPlatform/API/TodosNetwork.swift b/NetworkPlatform/API/TodosNetwork.swift deleted file mode 100644 index e9595d12..00000000 --- a/NetworkPlatform/API/TodosNetwork.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// TodosNetwork.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 16.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Domain -import RxSwift - -public final class TodosNetwork { - private let network: Network - - init(network: Network) { - self.network = network - } - - public func fetchTodos() -> Observable<[Todo]> { - return network.getItems("todos") - } - - public func fetchTodo(todoId: String) -> Observable { - return network.getItem("todos", itemId: todoId) - } -} diff --git a/NetworkPlatform/API/UsersNetwork.swift b/NetworkPlatform/API/UsersNetwork.swift deleted file mode 100644 index 0988f15a..00000000 --- a/NetworkPlatform/API/UsersNetwork.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// UsersNetwork.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 16.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Domain -import RxSwift - -public final class UsersNetwork { - private let network: Network - - init(network: Network) { - self.network = network - } - - public func fetchUsers() -> Observable<[User]> { - return network.getItems("users") - } - - public func fetchUser(userId: String) -> Observable { - return network.getItem("users", itemId: userId) - } -} diff --git a/NetworkPlatform/Cache/Cache.swift b/NetworkPlatform/Cache/Cache.swift deleted file mode 100644 index 3914393b..00000000 --- a/NetworkPlatform/Cache/Cache.swift +++ /dev/null @@ -1,133 +0,0 @@ -import Foundation -import RxSwift - -protocol AbstractCache { - associatedtype T - func save(object: T) -> Completable - func save(objects: [T]) -> Completable - func fetch(withID id: String) -> Maybe - func fetchObjects() -> Maybe<[T]> -} - -final class Cache: AbstractCache where T == T.Encoder.DomainType { - enum Error: Swift.Error { - case saveObject(T) - case saveObjects([T]) - case fetchObject(T.Type) - case fetchObjects(T.Type) - } - enum FileNames { - static var objectFileName: String { - return "\(T.self).dat" - } - static var objectsFileName: String { - return "\(T.self)s.dat" - } - } - - private let path: String - private let cacheScheduler = SerialDispatchQueueScheduler(internalSerialQueueName: "com.CleanAchitecture.Network.Cache.queue") - - init(path: String) { - self.path = path - } - - func save(object: T) -> Completable { - return Completable.create { (observer) -> Disposable in - guard let url = FileManager.default - .urls(for: .documentDirectory, in: .userDomainMask).first else { - observer(.completed) - return Disposables.create() - } - let path = url.appendingPathComponent(self.path) - .appendingPathComponent("\(object.uid)") - .appendingPathComponent(FileNames.objectFileName) - .absoluteString - - if NSKeyedArchiver.archiveRootObject(object.encoder, toFile: path) { - observer(.completed) - } else { - observer(.error(Error.saveObject(object))) - } - - return Disposables.create() - }.subscribeOn(cacheScheduler) - } - - func save(objects: [T]) -> Completable { - return Completable.create { (observer) -> Disposable in - guard let directoryURL = self.directoryURL() else { - observer(.completed) - return Disposables.create() - } - let path = directoryURL - .appendingPathComponent(FileNames.objectsFileName) - self.createDirectoryIfNeeded(at: directoryURL) - do { - try NSKeyedArchiver.archivedData(withRootObject: objects.map{ $0.encoder }) - .write(to: path) - observer(.completed) - } catch { - observer(.error(error)) - } - - return Disposables.create() - }.subscribeOn(cacheScheduler) - } - - func fetch(withID id: String) -> Maybe { - return Maybe.create { (observer) -> Disposable in - guard let url = FileManager.default - .urls(for: .documentDirectory, in: .userDomainMask).first else { - observer(.completed) - return Disposables.create() - } - let path = url.appendingPathComponent(self.path) - .appendingPathComponent("\(id)") - .appendingPathComponent(FileNames.objectFileName) - .absoluteString - - guard let object = NSKeyedUnarchiver.unarchiveObject(withFile: path) as? T.Encoder else { - observer(.completed) - return Disposables.create() - } - observer(MaybeEvent.success(object.asDomain())) - return Disposables.create() - }.subscribeOn(cacheScheduler) - } - - func fetchObjects() -> Maybe<[T]> { - return Maybe<[T]>.create { (observer) -> Disposable in - guard let directoryURL = self.directoryURL() else { - observer(.completed) - return Disposables.create() - } - let fileURL = directoryURL - .appendingPathComponent(FileNames.objectsFileName) - guard let objects = NSKeyedUnarchiver.unarchiveObject(withFile: fileURL.path) as? [T.Encoder] else { - observer(.completed) - return Disposables.create() - } - observer(MaybeEvent.success(objects.map { $0.asDomain() })) - return Disposables.create() - }.subscribeOn(cacheScheduler) - } - - private func directoryURL() -> URL? { - return FileManager.default - .urls(for: .documentDirectory, - in: .userDomainMask) - .first? - .appendingPathComponent(path) - } - - private func createDirectoryIfNeeded(at url: URL) { - do { - try FileManager.default.createDirectory(at: url, - withIntermediateDirectories: true, - attributes: nil) - } catch { - print("Cache Error createDirectoryIfNeeded \(error)") - } - } -} diff --git a/NetworkPlatform/Entries/Encodable/Encodable.swift b/NetworkPlatform/Entries/Encodable/Encodable.swift deleted file mode 100644 index 84be6cbc..00000000 --- a/NetworkPlatform/Entries/Encodable/Encodable.swift +++ /dev/null @@ -1,19 +0,0 @@ -protocol DomainConvertibleType { - associatedtype DomainType: Identifiable - - init(with domain: DomainType) - - func asDomain() -> DomainType -} - -protocol Identifiable { - var uid: String { get } -} - -typealias DomainConvertibleCoding = DomainConvertibleType - -protocol Encodable { - associatedtype Encoder: DomainConvertibleCoding - - var encoder: Encoder { get } -} diff --git a/NetworkPlatform/Entries/Post+Mapping.swift b/NetworkPlatform/Entries/Post+Mapping.swift deleted file mode 100644 index 0b620dd0..00000000 --- a/NetworkPlatform/Entries/Post+Mapping.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// Post+Mapping.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Domain - -extension Post: Identifiable {} - -extension Post { - func toJSON() -> [String: Any] { - return [ - "body": body, - "title": title, - "uid": uid, - "userId": userId, - "createdAt": createdAt - ] - } -} - -extension Post: Encodable { - var encoder: NETPost { - return NETPost(with: self) - } -} - -final class NETPost: NSObject, NSCoding, DomainConvertibleType { - struct Keys { - static let body = "body" - static let title = "title" - static let uid = "uid" - static let userId = "userId" - static let createdAt = "createdAt" - } - let body: String - let title: String - let uid: String - let userId: String - let createdAt: String - - init(with domain: Post) { - self.body = domain.body - self.title = domain.title - self.uid = domain.uid - self.userId = domain.userId - self.createdAt = domain.createdAt - } - - init?(coder aDecoder: NSCoder) { - guard - let body = aDecoder.decodeObject(forKey: Keys.body) as? String, - let title = aDecoder.decodeObject(forKey: Keys.title) as? String, - let uid = aDecoder.decodeObject(forKey: Keys.uid) as? String, - let userId = aDecoder.decodeObject(forKey: Keys.userId) as? String, - let createdAt = aDecoder.decodeObject(forKey: Keys.createdAt) as? String - else { - return nil - } - self.body = body - self.title = title - self.uid = uid - self.userId = userId - self.createdAt = createdAt - } - - func encode(with aCoder: NSCoder) { - aCoder.encode(body, forKey: Keys.body) - aCoder.encode(title, forKey: Keys.title) - aCoder.encode(uid, forKey: Keys.uid) - aCoder.encode(userId, forKey: Keys.userId) - aCoder.encode(createdAt, forKey: Keys.createdAt) - } - - func asDomain() -> Post { - return Post(body: body, - title: title, - uid: uid, - userId: userId, - createdAt: createdAt) - } -} diff --git a/NetworkPlatform/Info.plist b/NetworkPlatform/Info.plist deleted file mode 100644 index fbe1e6b3..00000000 --- a/NetworkPlatform/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - - - diff --git a/NetworkPlatform/Network.h b/NetworkPlatform/Network.h deleted file mode 100644 index 77ac183e..00000000 --- a/NetworkPlatform/Network.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// Network.h -// Network -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -#import - -//! Project version number for Network. -FOUNDATION_EXPORT double NetworkVersionNumber; - -//! Project version string for Network. -FOUNDATION_EXPORT const unsigned char NetworkVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/NetworkPlatform/Network/Network.swift b/NetworkPlatform/Network/Network.swift deleted file mode 100644 index f52096b6..00000000 --- a/NetworkPlatform/Network/Network.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// Network.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 16.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Foundation -import Alamofire -import Domain -import RxAlamofire -import RxSwift - -final class Network { - - private let endPoint: String - private let scheduler: ConcurrentDispatchQueueScheduler - - init(_ endPoint: String) { - self.endPoint = endPoint - self.scheduler = ConcurrentDispatchQueueScheduler(qos: DispatchQoS(qosClass: DispatchQoS.QoSClass.background, relativePriority: 1)) - } - - func getItems(_ path: String) -> Observable<[T]> { - let absolutePath = "\(endPoint)/\(path)" - return RxAlamofire - .data(.get, absolutePath) - .debug() - .observeOn(scheduler) - .map({ data -> [T] in - return try JSONDecoder().decode([T].self, from: data) - }) - } - - func getItem(_ path: String, itemId: String) -> Observable { - let absolutePath = "\(endPoint)/\(path)/\(itemId)" - return RxAlamofire - .data(.get, absolutePath) - .debug() - .observeOn(scheduler) - .map({ data -> T in - return try JSONDecoder().decode(T.self, from: data) - }) - } - - func postItem(_ path: String, parameters: [String: Any]) -> Observable { - let absolutePath = "\(endPoint)/\(path)" - return RxAlamofire - .request(.post, absolutePath, parameters: parameters) - .debug() - .observeOn(scheduler) - .data() - .map({ data -> T in - return try JSONDecoder().decode(T.self, from: data) - }) - } - - func updateItem(_ path: String, itemId: String, parameters: [String: Any]) -> Observable { - let absolutePath = "\(endPoint)/\(path)/\(itemId)" - return RxAlamofire - .request(.put, absolutePath, parameters: parameters) - .debug() - .observeOn(scheduler) - .data() - .map({ data -> T in - return try JSONDecoder().decode(T.self, from: data) - }) - } - - func deleteItem(_ path: String, itemId: String) -> Observable { - let absolutePath = "\(endPoint)/\(path)/\(itemId)" - return RxAlamofire - .request(.delete, absolutePath) - .debug() - .observeOn(scheduler) - .data() - .map({ data -> T in - return try JSONDecoder().decode(T.self, from: data) - }) - } -} diff --git a/NetworkPlatform/Network/NetworkProvider.swift b/NetworkPlatform/Network/NetworkProvider.swift deleted file mode 100644 index 737f9f64..00000000 --- a/NetworkPlatform/Network/NetworkProvider.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// NetworkProvider.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 16.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import Domain - -final class NetworkProvider { - private let apiEndpoint: String - - public init() { - apiEndpoint = "https://jsonplaceholder.typicode.com" - } - - public func makeAlbumsNetwork() -> AlbumsNetwork { - let network = Network(apiEndpoint) - return AlbumsNetwork(network: network) - } - - public func makeCommentsNetwork() -> CommentsNetwork { - let network = Network(apiEndpoint) - return CommentsNetwork(network: network) - } - - public func makePhotosNetwork() -> PhotosNetwork { - let network = Network(apiEndpoint) - return PhotosNetwork(network: network) - } - - public func makePostsNetwork() -> PostsNetwork { - let network = Network(apiEndpoint) - return PostsNetwork(network: network) - } - - public func makeTodosNetwork() -> TodosNetwork { - let network = Network(apiEndpoint) - return TodosNetwork(network: network) - } - - public func makeUsersNetwork() -> UsersNetwork { - let network = Network(apiEndpoint) - return UsersNetwork(network: network) - } -} diff --git a/NetworkPlatform/UseCases/PostsUseCase.swift b/NetworkPlatform/UseCases/PostsUseCase.swift deleted file mode 100644 index 87317d8d..00000000 --- a/NetworkPlatform/UseCases/PostsUseCase.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Foundation -import Domain -import RxSwift - -final class PostsUseCase: Domain.PostsUseCase where Cache: AbstractCache, Cache.T == Post { - private let network: PostsNetwork - private let cache: Cache - - init(network: PostsNetwork, cache: Cache) { - self.network = network - self.cache = cache - } - - func posts() -> Observable<[Post]> { - let fetchPosts = cache.fetchObjects().asObservable() - let stored = network.fetchPosts() - .flatMap { - return self.cache.save(objects: $0) - .asObservable() - .map(to: [Post].self) - .concat(Observable.just($0)) - } - - return fetchPosts.concat(stored) - } - - func save(post: Post) -> Observable { - return network.createPost(post: post) - .map { _ in } - } - - func delete(post: Post) -> Observable { - return network.deletePost(postId: post.uid).map({_ in}) - } -} - -struct MapFromNever: Error {} -extension ObservableType where E == Never { - func map(to: T.Type) -> Observable { - return self.flatMap { _ in - return Observable.error(MapFromNever()) - } - } -} diff --git a/NetworkPlatform/UseCases/UseCaseProvider.swift b/NetworkPlatform/UseCases/UseCaseProvider.swift deleted file mode 100644 index c4d81685..00000000 --- a/NetworkPlatform/UseCases/UseCaseProvider.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import Domain - -public final class UseCaseProvider: Domain.UseCaseProvider { - private let networkProvider: NetworkProvider - - public init() { - networkProvider = NetworkProvider() - } - - public func makePostsUseCase() -> Domain.PostsUseCase { - return PostsUseCase(network: networkProvider.makePostsNetwork(), - cache: Cache(path: "allPosts")) - } -} diff --git a/NetworkPlatformTests/Info.plist b/NetworkPlatformTests/Info.plist deleted file mode 100644 index 6c6c23c4..00000000 --- a/NetworkPlatformTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/NetworkPlatformTests/NetworkTests.swift b/NetworkPlatformTests/NetworkTests.swift deleted file mode 100644 index 05f63a0f..00000000 --- a/NetworkPlatformTests/NetworkTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// NetworkTests.swift -// NetworkTests -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import XCTest -@testable import NetworkPlatform - -class NetworkTests: XCTestCase { - - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testPerformanceExample() { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/Podfile b/Podfile deleted file mode 100644 index 09960700..00000000 --- a/Podfile +++ /dev/null @@ -1,84 +0,0 @@ -# Uncomment the next line to define a global platform for your project -platform :ios, '11.0' - -def rx_swift - pod 'RxSwift', '~> 4.0' -end - -def rx_cocoa - pod 'RxCocoa', '~> 4.0' -end - -def test_pods - pod 'RxTest' - pod 'RxBlocking' - pod 'Nimble' -end - - -target 'CleanArchitectureRxSwift' do - # Comment the next line if you're not using Swift and don't want to use dynamic frameworks - use_frameworks! - rx_cocoa - rx_swift - pod 'QueryKit' - target 'CleanArchitectureRxSwiftTests' do - inherit! :search_paths - test_pods - end - -end - -target 'CoreDataPlatform' do - # Comment the next line if you're not using Swift and don't want to use dynamic frameworks - use_frameworks! - rx_swift - pod 'QueryKit' - target 'CoreDataPlatformTests' do - inherit! :search_paths - test_pods - end - -end - -target 'Domain' do - # Comment the next line if you're not using Swift and don't want to use dynamic frameworks - use_frameworks! - rx_swift - - target 'DomainTests' do - inherit! :search_paths - test_pods - end - -end - -target 'NetworkPlatform' do - # Comment the next line if you're not using Swift and don't want to use dynamic frameworks - use_frameworks! - rx_swift - pod 'Alamofire' - pod 'RxAlamofire' - - target 'NetworkPlatformTests' do - inherit! :search_paths - test_pods - end - -end - -target 'RealmPlatform' do - # Comment the next line if you're not using Swift and don't want to use dynamic frameworks - use_frameworks! - rx_swift - pod 'RxRealm', '~> 0.7.1' - pod 'QueryKit' - pod 'RealmSwift', '~> 3.10' - pod 'Realm', '~> 3.10' - - target 'RealmPlatformTests' do - inherit! :search_paths - test_pods - end - -end diff --git a/Podfile.lock b/Podfile.lock deleted file mode 100644 index 5da6d865..00000000 --- a/Podfile.lock +++ /dev/null @@ -1,74 +0,0 @@ -PODS: - - Alamofire (4.7.3) - - Nimble (7.3.1) - - QueryKit (0.13.0) - - Realm (3.11.1): - - Realm/Headers (= 3.11.1) - - Realm/Headers (3.11.1) - - RealmSwift (3.11.1): - - Realm (= 3.11.1) - - RxAlamofire (4.3.0): - - RxAlamofire/Core (= 4.3.0) - - RxAlamofire/Core (4.3.0): - - Alamofire (~> 4.5) - - RxSwift (~> 4) - - RxAtomic (4.4.0) - - RxBlocking (4.4.0): - - RxAtomic (~> 4.4) - - RxSwift (~> 4.0) - - RxCocoa (4.4.0): - - RxSwift (~> 4.0) - - RxRealm (0.7.6): - - RealmSwift (~> 3.0) - - RxSwift (~> 4.0) - - RxSwift (4.4.0): - - RxAtomic (~> 4.4) - - RxTest (4.4.0): - - RxAtomic (~> 4.4) - - RxSwift (~> 4.0) - -DEPENDENCIES: - - Alamofire - - Nimble - - QueryKit - - Realm (~> 3.10) - - RealmSwift (~> 3.10) - - RxAlamofire - - RxBlocking - - RxCocoa (~> 4.0) - - RxRealm (~> 0.7.1) - - RxSwift (~> 4.0) - - RxTest - -SPEC REPOS: - https://github.com/cocoapods/specs.git: - - Alamofire - - Nimble - - QueryKit - - Realm - - RealmSwift - - RxAlamofire - - RxAtomic - - RxBlocking - - RxCocoa - - RxRealm - - RxSwift - - RxTest - -SPEC CHECKSUMS: - Alamofire: c7287b6e5d7da964a70935e5db17046b7fde6568 - Nimble: 04f732da099ea4d153122aec8c2a88fd0c7219ae - QueryKit: 406c42b9b4eb5f8dab380a9e5bd9ef656542d1f4 - Realm: 037c5919b9ceb59d6beed5d3b031096856b119b3 - RealmSwift: c9580133e73ef40ed340401af2dbc9a5790dfea7 - RxAlamofire: 09624d0f2d48ed8b686e4eb4cf68e28cbd2df556 - RxAtomic: eacf60db868c96bfd63320e28619fe29c179656f - RxBlocking: 138ad53217434444d6eeeb4fb406a45431d92e31 - RxCocoa: df63ebf7b9a70d6b4eeea407ed5dd4efc8979749 - RxRealm: 5379eddd74f8d617ca7681d1f8d144af25b432b0 - RxSwift: 5976ecd04fc2fefd648827c23de5e11157faa973 - RxTest: 19d03286bdc0a3aaea5d61d4cde31fdf4bb8a5ba - -PODFILE CHECKSUM: 8587d87d88ab80217209b0a3b29c2c6130be8755 - -COCOAPODS: 1.5.3 diff --git a/Projects/Core/ApolloExtensions/ApolloClient+Extensions.swift b/Projects/Core/ApolloExtensions/ApolloClient+Extensions.swift new file mode 100644 index 00000000..3cee0832 --- /dev/null +++ b/Projects/Core/ApolloExtensions/ApolloClient+Extensions.swift @@ -0,0 +1,30 @@ +import Apollo +import ApolloAPI +import Foundation +import Combine + +public extension ApolloClient { + func fetch(query: Query) async throws -> Query.Data { + let holder = CancellableHolder() + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { continuation in + holder.value = self.fetch(query: query) { result in + switch result { + case .success(let gqlResutl): + if let data = gqlResutl.data { + continuation.resume(returning: data) + } else if let error = gqlResutl.errors?.first { + continuation.resume(throwing: error) + } else { + continuation.resume(throwing: NoDataError()) + } + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } onCancel: { + holder.cancel() + } + } +} diff --git a/Projects/Core/ApolloExtensions/CancellableHolder.swift b/Projects/Core/ApolloExtensions/CancellableHolder.swift new file mode 100644 index 00000000..7544b313 --- /dev/null +++ b/Projects/Core/ApolloExtensions/CancellableHolder.swift @@ -0,0 +1,22 @@ +import Foundation +import Apollo + +final class CancellableHolder: @unchecked Sendable { + private var lock = NSRecursiveLock() + private var innerCancellable: Cancellable? + + private func synced(_ action: () throws -> Result) rethrows -> Result { + lock.lock() + defer { lock.unlock() } + return try action() + } + + var value: Cancellable? { + get { synced { innerCancellable } } + set { synced { innerCancellable = newValue } } + } + + func cancel() { + synced { innerCancellable?.cancel() } + } +} diff --git a/Projects/Core/ApolloExtensions/NoDataError.swift b/Projects/Core/ApolloExtensions/NoDataError.swift new file mode 100644 index 00000000..3197a317 --- /dev/null +++ b/Projects/Core/ApolloExtensions/NoDataError.swift @@ -0,0 +1,4 @@ + +public struct NoDataError: Error { + public init() {} +} diff --git a/Projects/Core/DependenciesMacro/InvertedDependencyMacro.swift b/Projects/Core/DependenciesMacro/InvertedDependencyMacro.swift new file mode 100644 index 00000000..0fe9ccf2 --- /dev/null +++ b/Projects/Core/DependenciesMacro/InvertedDependencyMacro.swift @@ -0,0 +1,9 @@ +import SwiftCompilerPlugin +import SwiftSyntaxMacros + +@main +struct Plugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + InvertedDependency.self + ] +} diff --git a/Projects/Core/DependenciesMacros/InvertedDependency.swift b/Projects/Core/DependenciesMacros/InvertedDependency.swift new file mode 100644 index 00000000..2c0b0495 --- /dev/null +++ b/Projects/Core/DependenciesMacros/InvertedDependency.swift @@ -0,0 +1,69 @@ +import SwiftSyntax +import SwiftSyntaxMacros +import SwiftDiagnostics + +public struct InvertedDependency: Macro { + public func expand( + declaration: DeclSyntax, + context: MacroExpansionContext + ) throws -> DeclSyntax { + guard let protocolDecl = declaration.as(ProtocolDeclSyntax.self) else { + throw DependencyKeyMacroErrors.shouldBeAttachedToAProtocol + } + + // Retrieve the protocol name + let protocolName = protocolDecl.name.text + + // Generate the dependency key enum and unimplemented struct + let enumCode = """ + enum \(protocolName)DependencyKey: TestDependencyKey { + struct Unimplemented: \(protocolName) { + \(protocolDecl.memberBlock.members.compactMap { member -> String? in + guard let funcDecl = member.decl.as(FunctionDeclSyntax.self) else { return nil } + let funcName = funcDecl.name.text + let returnType = funcDecl.signature.returnClause?.description ?? "" + let isThrowing = funcDecl.signature.effectSpecifiers?.throwsClause != nil + let throwsAttribute = isThrowing ? "throws" : "" + return "func \(funcName)\(funcDecl.signature.parameterClause) \(throwsAttribute)\(returnType) { unimplemented(#function) }" + } + .joined(separator: "\n")) + } + + static var testValue: \(protocolName) { + Unimplemented() + } + } + """ + + // Generate the DependencyValues extension + let extensionCode = """ + public extension DependencyValues { + var \(protocolName.firstLowercased()): \(protocolName) { + get { self[\(protocolName)DependencyKey.self] } + set { self[\(protocolName)DependencyKey.self] = newValue } + } + } + """ + + return DeclSyntax(stringLiteral: "\(enumCode)\n\n\(extensionCode)") + } +} + +fileprivate extension String { + /// Returns the string with the first character lowercased, for naming conventions. + func firstLowercased() -> String { + return prefix(1).lowercased() + dropFirst() + } +} + +struct TextMessage: DiagnosticMessage { + var message: String + + var diagnosticID: SwiftDiagnostics.MessageID + + var severity: SwiftDiagnostics.DiagnosticSeverity +} + +enum DependencyKeyMacroErrors: Error { + case shouldBeAttachedToAProtocol +} diff --git a/Projects/Core/DependenciesMacros/Plugin.swift b/Projects/Core/DependenciesMacros/Plugin.swift new file mode 100644 index 00000000..0fe9ccf2 --- /dev/null +++ b/Projects/Core/DependenciesMacros/Plugin.swift @@ -0,0 +1,9 @@ +import SwiftCompilerPlugin +import SwiftSyntaxMacros + +@main +struct Plugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + InvertedDependency.self + ] +} diff --git a/Projects/Core/FileCache/FileCache.swift b/Projects/Core/FileCache/FileCache.swift new file mode 100644 index 00000000..d4ee8226 --- /dev/null +++ b/Projects/Core/FileCache/FileCache.swift @@ -0,0 +1,63 @@ +import Foundation + +public final class FileCache { + private let fileManager = FileManager.default + private let directory: String + + public init(name: String) { + self.directory = "\(Bundle.main.bundleIdentifier ?? "")/" + (name.hasPrefix("/") ? String(name.dropFirst()) : name) + } + + public func loadFile(path: String) throws -> Data { + let fileURL = directoryURL.appendingPathComponent(path) + return try Data(contentsOf: fileURL) + } + + public func persist(data: Data, path: String) throws { + let path = path.hasPrefix("/") ? String(path.dropFirst()) : path + try createDirectoryIfNeeded() + let fileURL = directoryURL.appendingPathComponent(path) + let fileDirectoryURL = fileURL.deletingLastPathComponent() + try createDirectoryIfNeeded(for: fileDirectoryURL) + + if fileManager.fileExists(atPath: fileURL.path) { + try fileManager.removeItem(at: fileURL) + } + + try data.write(to: fileURL, options: .atomic) + } + + public func exists(atPath path: String) -> Bool { + let fileURL = directoryURL.appendingPathComponent(path) + return fileManager.fileExists(atPath: fileURL.path) + } + + public func persist(item: T, encoder: JSONEncoder, path: String) throws { + let data = try encoder.encode(item) + try persist(data: data, path: path) + } + + private func createDirectoryIfNeeded() throws { + if fileManager.fileExists(atPath: directoryURL.path) == false { + try fileManager.createDirectory( + at: directoryURL, + withIntermediateDirectories: true, + attributes: nil + ) + } + } + + private func createDirectoryIfNeeded(for url: URL) throws { + if fileManager.fileExists(atPath: url.path) == false { + try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) + } + } + + private var directoryURL: URL { + cacheDirectory().appendingPathComponent(directory) + } + + private func cacheDirectory() -> URL { + return fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0] + } +} diff --git a/Projects/Core/HTTPClient/HTTPClient.swift b/Projects/Core/HTTPClient/HTTPClient.swift new file mode 100644 index 00000000..1fa157ef --- /dev/null +++ b/Projects/Core/HTTPClient/HTTPClient.swift @@ -0,0 +1,80 @@ +import Foundation + +public protocol DataFetching { + func fetch(resource: Resource) async throws -> Data +} + +public final class HTTPClient: DataFetching { + private let session: URLSessionProtocol + private let environment: Environment + private let urlComponentsInterceptor: URLComponentsInterceptor + + public init( + session: URLSessionProtocol = URLSession.shared, + environment: Environment, + urlComponentsInterceptor: URLComponentsInterceptor + ) { + self.session = session + self.environment = environment + self.urlComponentsInterceptor = urlComponentsInterceptor + } + + public func fetch(resource: Resource) async throws -> Data { + let request = request(for: resource) + + do { + let (data, response) = try await session.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse, (200 ... 299).contains(httpResponse.statusCode) else { + throw NetworkError.invalidResponse + } + + return data + } catch let error as URLError where error.code == .notConnectedToInternet { + throw NetworkError.notConnectedToInternet + } catch let error as URLError where error.code == .cancelled { + throw NetworkError.cancelled + } catch let error as NetworkError { + throw error + } catch { + throw NetworkError.networkError(error) + } + } + + private func request(for resource: Resource) -> URLRequest { + var components = URLComponents() + + components.scheme = environment.schema + components.host = environment.host + components.path = "/" + environment.version + resource.path + components.queryItems = resource.query.map { key, value in URLQueryItem(name: key, value: value) } + + urlComponentsInterceptor.modify(components: &components) + + var request = URLRequest(url: components.url!) + request.httpMethod = resource.method.rawValue + + return request + } +} + +public enum NetworkError: Error { + case networkError(Error) + case invalidResponse + case cancelled + case notConnectedToInternet +} + +public extension HTTPClient { + struct Environment { + let schema: String + let host: String + let version: String + + public init(schema: String, host: String, version: String) { + self.schema = schema + self.host = host + self.version = version + } + } +} diff --git a/Projects/Core/HTTPClient/HTTPMethod.swift b/Projects/Core/HTTPClient/HTTPMethod.swift new file mode 100644 index 00000000..23dc7e58 --- /dev/null +++ b/Projects/Core/HTTPClient/HTTPMethod.swift @@ -0,0 +1,5 @@ +public enum HTTPMethod: String { + case GET + case POST + case DELETE +} diff --git a/Projects/Core/HTTPClient/Resource.swift b/Projects/Core/HTTPClient/Resource.swift new file mode 100644 index 00000000..913f12a5 --- /dev/null +++ b/Projects/Core/HTTPClient/Resource.swift @@ -0,0 +1,14 @@ +import Foundation + +public struct Resource { + + public let path: String + public let method: HTTPMethod + public let query: [String : String] + + public init(path: String, method: HTTPMethod = .GET, query: [String: String] = [:]) { + self.path = path + self.method = method + self.query = query + } +} diff --git a/Projects/Core/HTTPClient/URLComponentsInterceptor.swift b/Projects/Core/HTTPClient/URLComponentsInterceptor.swift new file mode 100644 index 00000000..46d7461f --- /dev/null +++ b/Projects/Core/HTTPClient/URLComponentsInterceptor.swift @@ -0,0 +1,5 @@ +import Foundation + +public protocol URLComponentsInterceptor { + func modify(components: inout URLComponents) +} diff --git a/Projects/Core/HTTPClient/URLSessionProtocol.swift b/Projects/Core/HTTPClient/URLSessionProtocol.swift new file mode 100644 index 00000000..83948d73 --- /dev/null +++ b/Projects/Core/HTTPClient/URLSessionProtocol.swift @@ -0,0 +1,11 @@ +import Foundation + +public protocol URLSessionProtocol { + func data(for request: URLRequest) async throws -> (Data, URLResponse) +} + +extension URLSession: URLSessionProtocol { + public func data(for request: URLRequest) async throws -> (Data, URLResponse) { + try await self.data(for: request, delegate: nil) + } +} diff --git a/Projects/Core/Project.swift b/Projects/Core/Project.swift new file mode 100644 index 00000000..6c476964 --- /dev/null +++ b/Projects/Core/Project.swift @@ -0,0 +1,4 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let project = ProjectName.Core.project diff --git a/Projects/Core/SwiftDataHelpers/ContextStore.swift b/Projects/Core/SwiftDataHelpers/ContextStore.swift new file mode 100644 index 00000000..a2d814cd --- /dev/null +++ b/Projects/Core/SwiftDataHelpers/ContextStore.swift @@ -0,0 +1,31 @@ +import SwiftData +import Foundation + +public final class ContextStore: Store { + private let modelContext: ModelContext + + public init(modelContainer: ModelContainer) { + self.modelContext = ModelContext(modelContainer) + } + + public func fetchAll(of type: T.Type, sortBy: [SortDescriptor]) throws -> [T] where T : PersistentModel { + let fetch = FetchDescriptor(sortBy: sortBy) + return try modelContext.fetch(fetch) + } + + public func fetchFirst(_ discriptor: FetchDescriptor) throws -> T? { + return try modelContext.fetch(discriptor).first + } + + public func fetch(_ discriptor: FetchDescriptor) throws -> [T] where T : PersistentModel { + return try modelContext.fetch(discriptor) + } + + public func insert(_ model: T) where T : PersistentModel { + modelContext.insert(model) + } + + public func save() throws { + try modelContext.save() + } +} diff --git a/Projects/Core/SwiftDataHelpers/DataStore.swift b/Projects/Core/SwiftDataHelpers/DataStore.swift new file mode 100644 index 00000000..a5dbe7b0 --- /dev/null +++ b/Projects/Core/SwiftDataHelpers/DataStore.swift @@ -0,0 +1,15 @@ +import Foundation +import SwiftData +import Dependencies + +public protocol Store { + func fetchAll(of type: T.Type, sortBy: [SortDescriptor]) throws -> [T] + + func fetch(_ discriptor: FetchDescriptor) throws -> [T] + + func fetchFirst(_ discriptor: FetchDescriptor) throws -> T? + + func insert(_ model: T) + + func save() throws +} diff --git a/Projects/Domain/AnimeDomain/Entities/DiscoverMedia.swift b/Projects/Domain/AnimeDomain/Entities/DiscoverMedia.swift new file mode 100644 index 00000000..88a7a26d --- /dev/null +++ b/Projects/Domain/AnimeDomain/Entities/DiscoverMedia.swift @@ -0,0 +1,60 @@ +import Foundation + +public struct DiscoverMedia: Hashable { + public let id: Int + public let startDate: Date? + public let endDate: Date? + public let coverImageURL: URL? + public let title: String + public let description: String + public let averageScore: Int + + public init( + id: Int, + startDate: Date?, + endDate: Date?, + coverImageURL: URL?, + title: String, + description: String, + averageScore: Int + ) { + self.id = id + self.startDate = startDate + self.endDate = endDate + self.coverImageURL = coverImageURL + self.title = title + self.description = description + self.averageScore = averageScore + } +} + +public enum DiscoverAnimeFilter: Hashable, CaseIterable { + case allTimePopular + case trending + case topRated +} + +public enum MediaType: Hashable, CaseIterable { + case anime + case manga +} + +#if DEBUG + +extension DiscoverMedia { + public static var sample: DiscoverMedia { + DiscoverMedia( + id: 1, + startDate: Date(), + endDate: Date(), + coverImageURL: URL(string: "ttps://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1-CXtrrkMpJ8Zq.png"), + title: "Cowboy Bebop", + description: """ + Enter a world in the distant future, where Bounty Hunters roam the solar system. Spike and Jet, bounty hunting partners, set out on journeys in an ever struggling effort to win bounty rewards to survive.

\nWhile traveling, they meet up with other very interesting people. Could Faye, the beautiful and ridiculously poor gambler, Edward, the computer genius, and Ein, the engineered dog be a good addition to the group? + """, + averageScore: 86 + ) + } +} + +#endif diff --git a/Projects/Domain/AnimeDomain/Entities/MediaDetail.swift b/Projects/Domain/AnimeDomain/Entities/MediaDetail.swift new file mode 100644 index 00000000..f87c2b26 --- /dev/null +++ b/Projects/Domain/AnimeDomain/Entities/MediaDetail.swift @@ -0,0 +1,76 @@ +import Foundation + +public struct MediaDetail: Equatable { + public let id: Int + public let coverImage: String? + public let genres: [String] + public let duration: Int + public let startDate: Date? + public let popularity: Int + public let averageScore: Int + public let description: String + public let bannerImage: String? + public let characters: [Character] + public let recommendations: [DiscoverMedia] + public let title: String + public let type: String + public let trailerURL: URL? + + public init( + id: Int, + coverImage: String?, + trailerURL: URL?, + genres: [String], + duration: Int, + startDate: Date?, + type: String, + popularity: Int, + averageScore: Int, + description: String, + bannerImage: String?, + characters: [Character], + title: String, + recommendations: [DiscoverMedia] + ) { + self.id = id + self.coverImage = coverImage + self.trailerURL = trailerURL + self.genres = genres + self.duration = duration + self.startDate = startDate + self.popularity = popularity + self.averageScore = averageScore + self.description = description + self.bannerImage = bannerImage + self.characters = characters + self.title = title + self.type = type + self.recommendations = recommendations + } +} + +public extension MediaDetail { + struct Character: Equatable { + public let id: Int + public let name: String? + public let image: String? + + public init(id: Int, name: String?, image: String?) { + self.id = id + self.name = name + self.image = image + } + } + + struct RecommendedMedia: Equatable { + public let id: Int + public let title: String + public let coverImage: String? + + public init(id: Int, title: String, coverImage: String?) { + self.id = id + self.title = title + self.coverImage = coverImage + } + } +} diff --git a/Projects/Domain/AnimeDomain/Entities/MediaWatchlist.swift b/Projects/Domain/AnimeDomain/Entities/MediaWatchlist.swift new file mode 100644 index 00000000..3cd4cdc8 --- /dev/null +++ b/Projects/Domain/AnimeDomain/Entities/MediaWatchlist.swift @@ -0,0 +1,36 @@ +import Foundation + +public struct MediaWatchlist { + public var media: [DiscoverMedia] + + public init(media: [DiscoverMedia]) { + self.media = media + } +} + +public struct MediaSeenlist { + public var media: [DiscoverMedia] + + public init(media: [DiscoverMedia]) { + self.media = media + } +} + +public struct MediaList { + public let id: UUID + public var name: String + public var imagePath: String? + public var media: [DiscoverMedia] + + public init( + id: UUID = .init(), + name: String, + imagePath: String?, + media: [DiscoverMedia] + ) { + self.id = id + self.name = name + self.imagePath = imagePath + self.media = media + } +} diff --git a/Projects/Domain/AnimeDomain/Entities/PageInfo.swift b/Projects/Domain/AnimeDomain/Entities/PageInfo.swift new file mode 100644 index 00000000..5a7d031c --- /dev/null +++ b/Projects/Domain/AnimeDomain/Entities/PageInfo.swift @@ -0,0 +1,9 @@ +public struct PageInfo: Equatable { + public let currentPage: Int + public let hasNextPage: Bool + + public init(currentPage: Int, hasNextPage: Bool) { + self.currentPage = currentPage + self.hasNextPage = hasNextPage + } +} diff --git a/Projects/Domain/AnimeDomain/Entities/Paged.swift b/Projects/Domain/AnimeDomain/Entities/Paged.swift new file mode 100644 index 00000000..7280b297 --- /dev/null +++ b/Projects/Domain/AnimeDomain/Entities/Paged.swift @@ -0,0 +1,13 @@ +import Foundation + +public struct Paged { + public let response: T + public let pageInfo: PageInfo + + public init(response: T, pageInfo: PageInfo) { + self.response = response + self.pageInfo = pageInfo + } +} + +extension Paged: Equatable where T: Equatable {} diff --git a/Projects/Domain/AnimeDomain/UseCases/AnimeDetailUseCase.swift b/Projects/Domain/AnimeDomain/UseCases/AnimeDetailUseCase.swift new file mode 100644 index 00000000..981217e5 --- /dev/null +++ b/Projects/Domain/AnimeDomain/UseCases/AnimeDetailUseCase.swift @@ -0,0 +1,30 @@ +import Foundation +import Dependencies +import XCTestDynamicOverlay + +public protocol AnimeDetailUseCase { + func fetchBy(id: Int) async throws -> MediaDetail +} + +public enum AnimeDetailUseCaseDependencyKey: TestDependencyKey { + struct Unimplemented: AnimeDetailUseCase { + func fetchBy(id: Int) throws -> MediaDetail { + fatalError() + } + } + + public static var testValue: AnimeDetailUseCase { + Unimplemented() + } +} + +extension DependencyValues { + public var animeDetailUseCase: AnimeDetailUseCase { + get { + self[AnimeDetailUseCaseDependencyKey.self] + } + set { + self[AnimeDetailUseCaseDependencyKey.self] = newValue + } + } +} diff --git a/Projects/Domain/AnimeDomain/UseCases/DiscoverAnimeUseCase.swift b/Projects/Domain/AnimeDomain/UseCases/DiscoverAnimeUseCase.swift new file mode 100644 index 00000000..071f89f1 --- /dev/null +++ b/Projects/Domain/AnimeDomain/UseCases/DiscoverAnimeUseCase.swift @@ -0,0 +1,48 @@ +import Foundation +import Dependencies +import XCTestDynamicOverlay + +public protocol DiscoverAnimeUseCase { + func fetch( + page: Int, + perPage: Int, + filter: DiscoverAnimeFilter, + mediaType: MediaType, + genres: [String]? + ) async throws -> Paged<[DiscoverMedia]> +} + +public enum DiscoverAnimeUseCaseDependencyKey: TestDependencyKey { + struct Unimplemented: DiscoverAnimeUseCase { + func fetch( + page: Int, + perPage: Int, + filter: DiscoverAnimeFilter, + mediaType: MediaType, + genres: [String]? + ) throws -> Paged<[DiscoverMedia]> { + unimplemented( + #function, + placeholder: Paged<[DiscoverMedia]>( + response: [], + pageInfo: PageInfo(currentPage: 0, hasNextPage: false) + ) + ) + } + } + + public static var testValue: DiscoverAnimeUseCase { + Unimplemented() + } +} + +extension DependencyValues { + public var discoverAnimeUseCase: DiscoverAnimeUseCase { + get { + self[DiscoverAnimeUseCaseDependencyKey.self] + } + set { + self[DiscoverAnimeUseCaseDependencyKey.self] = newValue + } + } +} diff --git a/Projects/Domain/AnimeDomain/UseCases/MediaSeenlistUseCase.swift b/Projects/Domain/AnimeDomain/UseCases/MediaSeenlistUseCase.swift new file mode 100644 index 00000000..6284223b --- /dev/null +++ b/Projects/Domain/AnimeDomain/UseCases/MediaSeenlistUseCase.swift @@ -0,0 +1,46 @@ +import Dependencies +import XCTestDynamicOverlay + +public protocol MediaSeenlistUseCase { + func contains(media: DiscoverMedia) -> Bool + @discardableResult + func add(media: DiscoverMedia) throws -> MediaSeenlist + @discardableResult + func remove(media: DiscoverMedia) throws -> MediaSeenlist + func getSeenList() throws -> MediaSeenlist +} + +public enum MovieSeenlistUseCaseDependencyKey: TestDependencyKey { + public static var testValue: MediaSeenlistUseCase { + struct Unimplemented: MediaSeenlistUseCase { + func contains(media: DiscoverMedia) -> Bool { + unimplemented(#function, placeholder: false) + } + + func add(media: DiscoverMedia) throws -> MediaSeenlist { + unimplemented(#function, placeholder: MediaSeenlist(media: [])) + } + + func remove(media: DiscoverMedia) throws -> MediaSeenlist { + unimplemented(#function, placeholder: MediaSeenlist(media: [])) + } + + func getSeenList() throws -> MediaSeenlist { + unimplemented(#function, placeholder: MediaSeenlist(media: [])) + } + } + + return Unimplemented() + } +} + +extension DependencyValues { + public var mediaSeenlistUseCase: MediaSeenlistUseCase { + get { + self[MovieSeenlistUseCaseDependencyKey.self] + } + set { + self[MovieSeenlistUseCaseDependencyKey.self] = newValue + } + } +} diff --git a/Projects/Domain/AnimeDomain/UseCases/MediaWatchlistUseCase.swift b/Projects/Domain/AnimeDomain/UseCases/MediaWatchlistUseCase.swift new file mode 100644 index 00000000..beead53a --- /dev/null +++ b/Projects/Domain/AnimeDomain/UseCases/MediaWatchlistUseCase.swift @@ -0,0 +1,46 @@ +import Dependencies +import XCTestDynamicOverlay + +public protocol MediaWatchlistUseCase { + func contains(media: DiscoverMedia) -> Bool + @discardableResult + func add(media: DiscoverMedia) throws -> MediaWatchlist + @discardableResult + func remove(media: DiscoverMedia) throws -> MediaWatchlist + func getWatchlist() throws -> MediaWatchlist +} + +public enum MediaWatchlistUseCaseDependencyKey: TestDependencyKey { + public static var testValue: MediaWatchlistUseCase { + struct Unimplemented: MediaWatchlistUseCase { + func contains(media: DiscoverMedia) -> Bool { + unimplemented(#function, placeholder: false) + } + + func add(media: DiscoverMedia) throws -> MediaWatchlist { + unimplemented(#function, placeholder: MediaWatchlist(media: [])) + } + + func remove(media: DiscoverMedia) throws -> MediaWatchlist { + unimplemented(#function, placeholder: MediaWatchlist(media: [])) + } + + func getWatchlist() throws -> MediaWatchlist { + unimplemented(#function, placeholder: MediaWatchlist(media: [])) + } + } + + return Unimplemented() + } +} + +extension DependencyValues { + public var mediaWatchlistUseCase: MediaWatchlistUseCase { + get { + self[MediaWatchlistUseCaseDependencyKey.self] + } + set { + self[MediaWatchlistUseCaseDependencyKey.self] = newValue + } + } +} diff --git a/Projects/Domain/MoviesDomain/Entities/Keyword.swift b/Projects/Domain/MoviesDomain/Entities/Keyword.swift new file mode 100644 index 00000000..9363b9e1 --- /dev/null +++ b/Projects/Domain/MoviesDomain/Entities/Keyword.swift @@ -0,0 +1,6 @@ +import Foundation + +public struct Keyword: Codable, Hashable { + public let id: Int + public let name: String +} diff --git a/Projects/Domain/MoviesDomain/Entities/Movie.swift b/Projects/Domain/MoviesDomain/Entities/Movie.swift new file mode 100644 index 00000000..932164db --- /dev/null +++ b/Projects/Domain/MoviesDomain/Entities/Movie.swift @@ -0,0 +1,117 @@ +import Foundation +import Tagged + +public typealias MovieID = Tagged + +public struct Movie: Hashable, Codable { + public var adult: Bool + public var backdropPath: String? + public var id: MovieID + public var overview: String + public var popularity: Double + public var posterPath: String? + @SafeDateDecoding + public var releaseDate: Date? + public var title: String + public var video: Bool + public var voteAverage: Double + public var voteCount: Int + + public init(adult: Bool, backdropPath: String?, id: MovieID, overview: String, popularity: Double, posterPath: String?, releaseDate: Date?, title: String, video: Bool, voteAverage: Double, voteCount: Int) { + self.adult = adult + self.backdropPath = backdropPath + self.id = id + self.overview = overview + self.popularity = popularity + self.posterPath = posterPath + self.releaseDate = releaseDate + self.title = title + self.video = video + self.voteAverage = voteAverage + self.voteCount = voteCount + } + + enum CodingKeys: String, CodingKey { + case adult + case backdropPath = "backdrop_path" + case id + case overview + case popularity + case posterPath = "poster_path" + case releaseDate = "release_date" + case title + case video + case voteAverage = "vote_average" + case voteCount = "vote_count" + } +} + +#if DEBUG +extension Movie { + static let formatter = { + let formatter = DateFormatter() + formatter.dateFormat = "YYYY-MM-DD" + return formatter + }() + + public static var sample: Movie { + Movie( + adult: false, + backdropPath: "/stKGOm8UyhuLPR9sZLjs5AkmncA.jpg", + id: 1022789, + overview: """ + Teenager Riley's mind headquarters is undergoing a sudden demolition to make room for something entirely unexpected: new Emotions! Joy, Sadness, Anger, Fear and Disgust, who’ve long been running a successful operation by all accounts, aren’t sure how to feel when Anxiety shows up. And it looks like she’s not alone. + """, + popularity: 3553.5, + posterPath: "/vpnVM9B6NMmQpWeZvzLvDESb2QY.jpg", + releaseDate: Movie.formatter.date(from: "2024-06-11") ?? Date(), + title: "Inside Out 2", + video: false, + voteAverage: 7.591, + voteCount: 115 + ) + } +} +#endif + +public extension CodingUserInfoKey { + static let dateFormatter = CodingUserInfoKey(rawValue: "dateFormatter")! +} + +@propertyWrapper +public struct SafeDateDecoding: Codable, Hashable { + public var wrappedValue: Date? + + public init(wrappedValue: Date? = nil) { + self.wrappedValue = wrappedValue + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + guard let dateFormatter = decoder.userInfo[.dateFormatter] as? DateFormatter else { + throw DecodingError.valueNotFound( + DateFormatter.self, + DecodingError.Context( + codingPath: [], + debugDescription: "DateFormatter is not set in userInfo" + ) + ) + } + self.wrappedValue = string.isEmpty ? nil : dateFormatter.date(from: string) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + guard let dateFormatter = encoder.userInfo[.dateFormatter] as? DateFormatter else { + throw DecodingError.valueNotFound( + DateFormatter.self, + DecodingError.Context( + codingPath: [], + debugDescription: "DateFormatter is not set in userInfo" + ) + ) + } + try container.encode(wrappedValue.map(dateFormatter.string(from:)) ?? "") + } +} diff --git a/Projects/Domain/MoviesDomain/Entities/MovieCast.swift b/Projects/Domain/MoviesDomain/Entities/MovieCast.swift new file mode 100644 index 00000000..de973e69 --- /dev/null +++ b/Projects/Domain/MoviesDomain/Entities/MovieCast.swift @@ -0,0 +1,7 @@ +import Foundation + +public struct MovieCast: Codable, Equatable { + public let id: Int + public let cast: [Person] + public let crew: [Person] +} diff --git a/Projects/Domain/MoviesDomain/Entities/MovieDetail.swift b/Projects/Domain/MoviesDomain/Entities/MovieDetail.swift new file mode 100644 index 00000000..4b7a7e97 --- /dev/null +++ b/Projects/Domain/MoviesDomain/Entities/MovieDetail.swift @@ -0,0 +1,65 @@ +import Foundation + +public struct MovieDetail: Equatable, Decodable { + public var adult: Bool + public var backdropPath: String? + public var belongsToCollection: Collection? + public var budget: Int + public var genres: [Genre]? + public var homepage: String? + public var id: MovieID + public var originCountry: [String] + public var originalLanguage: String + public var originalTitle: String + public var overview: String + public var popularity: Double + public var posterPath: String? + public var productionCompanies: [ProductionCompany]? + public var productionCountries: [ProductionCountry]? + public var releaseDate: Date + public var revenue: Int + public var runtime: Int + public var spokenLanguages: [SpokenLanguage] + public var status: String + public var tagline: String + public var title: String + public var video: Bool + public var voteAverage: Double + public var voteCount: Int + public var keywords: Keywords? +} + +public extension MovieDetail { + struct Collection: Equatable, Decodable { + public var id: Int + public var name: String + public var posterPath: String? + public var backdropPath: String? + } + + struct Genre: Equatable, Decodable { + public var id: Int + public var name: String + } + + struct ProductionCompany: Equatable, Decodable { + public var id: Int + public var logoPath: String? + public var name: String + public var originCountry: String + } + + struct ProductionCountry: Equatable, Decodable { + public var iso3166_1: String? + public var name: String + } + + struct SpokenLanguage: Equatable, Decodable { + public var englishName: String + public var name: String + } + + struct Keywords: Codable, Hashable { + public var keywords: [Keyword]? + } +} diff --git a/Projects/Domain/MoviesDomain/Entities/MovieImage.swift b/Projects/Domain/MoviesDomain/Entities/MovieImage.swift new file mode 100644 index 00000000..57724450 --- /dev/null +++ b/Projects/Domain/MoviesDomain/Entities/MovieImage.swift @@ -0,0 +1,7 @@ +public struct MovieImage { + public var path: String + + public init(path: String) { + self.path = path + } +} diff --git a/Projects/Domain/MoviesDomain/Entities/MovieWatchlist.swift b/Projects/Domain/MoviesDomain/Entities/MovieWatchlist.swift new file mode 100644 index 00000000..dd61f895 --- /dev/null +++ b/Projects/Domain/MoviesDomain/Entities/MovieWatchlist.swift @@ -0,0 +1,36 @@ +import Foundation + +public struct MovieWatchlist: Codable { + public var movies: [Movie] + + public init(movies: [Movie]) { + self.movies = movies + } +} + +public struct MovieSeenList: Codable { + public var movies: [Movie] + + public init(movies: [Movie]) { + self.movies = movies + } +} + +public struct MovieList: Codable { + public let id: UUID + public var name: String + public var imagePath: String? + public var movies: [Movie] + + public init( + id: UUID = .init(), + name: String, + imagePath: String?, + movies: [Movie] + ) { + self.id = id + self.name = name + self.imagePath = imagePath + self.movies = movies + } +} diff --git a/Projects/Domain/MoviesDomain/Entities/OfflineError.swift b/Projects/Domain/MoviesDomain/Entities/OfflineError.swift new file mode 100644 index 00000000..5974db76 --- /dev/null +++ b/Projects/Domain/MoviesDomain/Entities/OfflineError.swift @@ -0,0 +1,4 @@ + +public struct OfflineError: Error { + public init() {} +} diff --git a/Projects/Domain/MoviesDomain/Entities/PageResult.swift b/Projects/Domain/MoviesDomain/Entities/PageResult.swift new file mode 100644 index 00000000..ff19ec48 --- /dev/null +++ b/Projects/Domain/MoviesDomain/Entities/PageResult.swift @@ -0,0 +1,20 @@ +public struct PageResult: Codable { + public var page: Int + public var results: [Item] + public var totalPages: Int + public var totalResults: Int + + enum CodingKeys: String, CodingKey { + case page = "page" + case results = "results" + case totalPages = "total_pages" + case totalResults = "total_results" + } + + init(page: Int, results: [Item], totalPages: Int, totalResults: Int) { + self.page = page + self.results = results + self.totalPages = totalPages + self.totalResults = totalResults + } +} diff --git a/Projects/Domain/MoviesDomain/Entities/Person.swift b/Projects/Domain/MoviesDomain/Entities/Person.swift new file mode 100644 index 00000000..2a6ec501 --- /dev/null +++ b/Projects/Domain/MoviesDomain/Entities/Person.swift @@ -0,0 +1,40 @@ +import Foundation +import Tagged + +public typealias PersonID = Tagged + +public struct Person: Codable, Equatable { + public var id: PersonID + public var name: String + public var knownForDepartment: String? + public var originalName: String? + public var popularity: Double? + public var profilePath: String? + public var castID: Int? + public var character: String? + public var creditID: String? + public var department: String? + public var job: String? + + enum CodingKeys: String, CodingKey { + case id + case knownForDepartment = "known_for_department" + case name + case originalName = "original_name" + case popularity + case profilePath = "profile_path" + case castID = "cast_id" + case character + case creditID = "credit_id" + case department + case job + } +} + +#if DEBUG +public extension Person { + static var example: Self { + Person(id: 42, name: "Tom Hardy") + } +} +#endif diff --git a/Projects/Domain/MoviesDomain/Entities/PersonDetails.swift b/Projects/Domain/MoviesDomain/Entities/PersonDetails.swift new file mode 100644 index 00000000..8a237707 --- /dev/null +++ b/Projects/Domain/MoviesDomain/Entities/PersonDetails.swift @@ -0,0 +1,72 @@ +import Foundation + +public struct PersonDetails: Codable { + public var adult: Bool + public var alsoKnownAs: [String] + public var biography: String + public var birthday: Date? + public var deathday: Date? + public var gender: Int + public var id: PersonID + public var imdbID: String? + public var knownForDepartment, name: String + public var placeOfBirth: String? + public var popularity: Double + public var profilePath: String? + public var images: Images + + public struct Images: Codable { + public var profiles: [Image] + } + + public struct Image: Codable { + public var filePath: String + } +} + +#if DEBUG +public extension PersonDetails { + static var example: Self { + PersonDetails( + adult: false, + alsoKnownAs: [ + "Edward Thomas Hardy", + "توم هاردي", + "톰 하디", + "トム・ハーディ", + "ทอม ฮาร์ดี", + "汤姆·哈迪", + "ტომ ჰარდი", + "Edward Thomas \"Tom\" Hardy", + "Έντουαρντ Τόμας \"Τομ\" Χάρντι", + "Έντουαρντ Τόμας Χάρντι", + "טום הארדי", + "Том Харді" + ], + biography: + """ + "Edward Thomas Hardy CBE (born 15 September 1977) is an English actor, producer, writer and former model. After studying acting at the Drama Centre London, he made his film debut in Ridley Scott's Black Hawk Down (2001). He has since been nominated for the Academy Award for Best Supporting Actor, two Critics' Choice Movie Awards and two British Academy Film Awards, receiving the 2011 BAFTA Rising Star Award.\n\nHardy has also appeared in films such as Star Trek: Nemesis (2002), RocknRolla (2008), Bronson (2008), Warrior (2011), Tinker Tailor Soldier Spy (2011), Lawless (2012), This Means War (2012), Locke (2013), The Drop (2014), and The Revenant (2015), for which he received a nomination for an Academy Award. In 2015, he portrayed \"Mad\" Max Rockatansky in Mad Max: Fury Road and both Kray twins in Legend. He has appeared in three Christopher Nolan films: Inception (2010) as Eames, The Dark Knight Rises (2012) as Bane, and Dunkirk (2017) as an RAF fighter-pilot. He starred as both Eddie Brock and Venom in the 2018 anti-hero film Venom and its sequel Venom: Let There Be Carnage (2021).\n\nHardy's television roles include the HBO war drama mini-series Band of Brothers (2001), the BBC historical drama mini-series The Virgin Queen (2005), Bill Sikes in the BBC's mini-series Oliver Twist (2007), Heathcliff in ITV's Wuthering Heights (2009), the Sky 1 drama series The Take (2009), and as Alfie Solomons in the BBC historical crime drama series Peaky Blinders (2014–present). He created, co-produced, and took the lead in the eight-part historical fiction series Taboo (2017) on BBC One and FX. In 2020, he also contributed narration work to the Amazon docuseries All or Nothing: Tottenham Hotspur.\n\nHardy has performed on both British and American stages. He was nominated for the Laurence Olivier Award for Most Promising Newcomer for his role as Skank in the production of In Arabia We'd All Be Kings (2003), and was awarded the 2003 Evening Standard Theatre Award for Outstanding Newcomer for his performances in both In Arabia We'd All Be Kings and Blood, in which he played Luca. He starred in the production of The Man of Mode (2007) and received positive reviews for his role in the play The Long Red Road (2010). Hardy is active in charity work and is an ambassador for the Prince's Trust. He was appointed a CBE in the 2018 Birthday Honours for services to drama.\n\nDescription above from the Wikipedia article Tom Hardy, licensed under CC-BY-SA, full list of contributors on Wikipedia." + """, + birthday: Date(timeIntervalSince1970: 0), + gender: 1, + id: 2524, + imdbID: "nm0362766", + knownForDepartment: "Acting", + name: "Tom Hardy", + placeOfBirth: "Hammersmith, London, England, UK", + popularity: 112.617, + profilePath: "/d81K0RH8UX7tZj49tZaQhZ9ewH.jpg", + images: Images( + profiles: [ + Image(filePath: "/d81K0RH8UX7tZj49tZaQhZ9ewH.jpg"), + Image(filePath: "/scbbuyWX3yuMjDlm1etAljrbCr0.jpg"), + Image(filePath: "/mHSmt9qu2JzEPqnVWCGViv9Stnn.jpg"), + Image(filePath: "/yVGF9FvDxTDPhGimTbZNfghpllA.jpg"), + Image(filePath: "/sGMA6pA2D6X0gun49igJT3piHs3.jpg") + ] + ) + ) + } +} + +#endif diff --git a/Projects/Domain/MoviesDomain/Gateway/DicoverMoviesRepository.swift b/Projects/Domain/MoviesDomain/Gateway/DicoverMoviesRepository.swift new file mode 100644 index 00000000..c5a101cc --- /dev/null +++ b/Projects/Domain/MoviesDomain/Gateway/DicoverMoviesRepository.swift @@ -0,0 +1,7 @@ +import Foundation + +public protocol DicoverMoviesRepository { + func movies(for reuqest: DiscoverMoviesRequest) throws -> [Movie] + + func save(movies: [Movie], for request: DiscoverMoviesRequest) throws +} diff --git a/Projects/Domain/MoviesDomain/Gateway/DiscoverMoviesGateway.swift b/Projects/Domain/MoviesDomain/Gateway/DiscoverMoviesGateway.swift new file mode 100644 index 00000000..7a7fd67a --- /dev/null +++ b/Projects/Domain/MoviesDomain/Gateway/DiscoverMoviesGateway.swift @@ -0,0 +1,12 @@ +import Foundation + +public protocol DiscoverMoviesGateway { + func fetch(request: DiscoverMoviesRequest) async throws -> PageResult +} + +public enum DiscoverMoviesRequest: Hashable, CaseIterable { + case nowPlaying + case popular + case topRated + case upcoming +} diff --git a/Projects/Domain/MoviesDomain/UseCases/DiscoverMoviesUseCase.swift b/Projects/Domain/MoviesDomain/UseCases/DiscoverMoviesUseCase.swift new file mode 100644 index 00000000..a60c9bb0 --- /dev/null +++ b/Projects/Domain/MoviesDomain/UseCases/DiscoverMoviesUseCase.swift @@ -0,0 +1,56 @@ +import Foundation +import Dependencies +import XCTestDynamicOverlay + +public protocol DiscoverMoviesUseCaseProtocol { + func fetch(request: DiscoverMoviesRequest, page: Int) async throws -> PageResult +} + +public final class DiscoverMoviesUseCase: DiscoverMoviesUseCaseProtocol { + private let gateway: DiscoverMoviesGateway + private let repository: DicoverMoviesRepository + + public init(gateway: DiscoverMoviesGateway, repository: DicoverMoviesRepository) { + self.gateway = gateway + self.repository = repository + } + + public func fetch(request: DiscoverMoviesRequest, page: Int) async throws -> PageResult { + do { + let pageResult = try await gateway.fetch(request: request) + if page == 1 { + try? await repository.save(movies: pageResult.results, for: request) + } + return pageResult + } catch { + if error is OfflineError { + let movies = try await repository.movies(for: request) + return PageResult(page: page, results: movies, totalPages: 0, totalResults: movies.count) + } + throw error + } + } +} + +public enum DiscoverMoviesUseCaseDependencyKey: TestDependencyKey { + struct Unimplemented: DiscoverMoviesUseCaseProtocol { + func fetch(request: DiscoverMoviesRequest, page: Int) async throws -> PageResult { + unimplemented(#function, placeholder: .init(page: 0, results: [], totalPages: 0, totalResults: 0)) + } + } + + public static var testValue: DiscoverMoviesUseCaseProtocol { + Unimplemented() + } +} + +extension DependencyValues { + public var discoverMoviesUseCase: DiscoverMoviesUseCaseProtocol { + get { + self[DiscoverMoviesUseCaseDependencyKey.self] + } + set { + self[DiscoverMoviesUseCaseDependencyKey.self] = newValue + } + } +} diff --git a/Projects/Domain/MoviesDomain/UseCases/MovieCreditsUseCase.swift b/Projects/Domain/MoviesDomain/UseCases/MovieCreditsUseCase.swift new file mode 100644 index 00000000..f96f0e0a --- /dev/null +++ b/Projects/Domain/MoviesDomain/UseCases/MovieCreditsUseCase.swift @@ -0,0 +1,30 @@ +import Foundation +import Dependencies +import XCTestDynamicOverlay + +public protocol MovieCreditsUseCase { + func fetchCast(movieID: MovieID) async throws -> MovieCast +} + +public enum MovieCreditsUseCaseDependencyKey: TestDependencyKey { + struct Unimplemented: MovieCreditsUseCase { + func fetchCast(movieID: MovieID) throws -> MovieCast { + fatalError("Unimplemented") + } + } + + public static var testValue: MovieCreditsUseCase { + Unimplemented() + } +} + +public extension DependencyValues { + var movieCreditsUseCase: MovieCreditsUseCase { + get { + self[MovieCreditsUseCaseDependencyKey.self] + } + set { + self[MovieCreditsUseCaseDependencyKey.self] = newValue + } + } +} diff --git a/Projects/Domain/MoviesDomain/UseCases/MovieDetailUseCase.swift b/Projects/Domain/MoviesDomain/UseCases/MovieDetailUseCase.swift new file mode 100644 index 00000000..c57ee8d4 --- /dev/null +++ b/Projects/Domain/MoviesDomain/UseCases/MovieDetailUseCase.swift @@ -0,0 +1,28 @@ +import Dependencies + +public protocol MovieDetailUseCaseProtocol { + func fetchDetail(for movieID: MovieID) async throws -> MovieDetail +} + +public enum MovieDetailUseCaseProtocolDependencyKey: TestDependencyKey { + struct Unimplemented: MovieDetailUseCaseProtocol { + func fetchDetail(for movieID: MovieID) throws -> MovieDetail { + fatalError("Unimplemented") + } + } + + public static var testValue: MovieDetailUseCaseProtocol { + Unimplemented() + } +} + +public extension DependencyValues { + var movieDetailUseCase: MovieDetailUseCaseProtocol { + get { + self[MovieDetailUseCaseProtocolDependencyKey.self] + } + set { + self[MovieDetailUseCaseProtocolDependencyKey.self] = newValue + } + } +} diff --git a/Projects/Domain/MoviesDomain/UseCases/MovieListUseCase.swift b/Projects/Domain/MoviesDomain/UseCases/MovieListUseCase.swift new file mode 100644 index 00000000..a0a3950a --- /dev/null +++ b/Projects/Domain/MoviesDomain/UseCases/MovieListUseCase.swift @@ -0,0 +1,147 @@ +import Dependencies + +public protocol MovieListUseCase { + func getCustomLists() throws -> [MovieList] + @discardableResult + func add(movie: Movie, to list: MovieList) throws -> MovieList + @discardableResult + func remove(movie: Movie, from list: MovieList) throws -> MovieList + @discardableResult + func create(name: String, imagePath: String?) throws -> MovieList + + func isMovieInMovieList(_ movie: Movie) -> Bool +} + +public enum MovieListUseCaseDependencyKey: TestDependencyKey { + struct Unimplemented: MovieListUseCase { + func getCustomLists() throws -> [MovieList] { + fatalError("Unimplemented") + } + + func add(movie: Movie, to list: MovieList) throws -> MovieList { + fatalError("Unimplemented") + } + + func remove(movie: Movie, from list: MovieList) throws -> MovieList { + fatalError("Unimplemented") + } + + func create(name: String, imagePath: String?) throws -> MovieList { + fatalError("Unimplemented") + } + + func searchListCover(query: String) async throws -> [MovieImage] { + fatalError("Unimplemented") + } + + func isMovieInMovieList(_ movie: Movie) -> Bool { + fatalError("Unimplemented") + } + } + + public static var testValue: MovieListUseCase { + Unimplemented() + } +} + +@MainActor +public protocol MovieWatchlistUseCase { + func contains(movie: Movie) -> Bool + @discardableResult + func add(movie: Movie) throws -> MovieWatchlist + @discardableResult + func remove(movie: Movie) throws -> MovieWatchlist + func getWatchlist() throws -> MovieWatchlist +} + +public enum MovieWatchlistUseCaseDependencyKey: TestDependencyKey { + @MainActor + struct Unimplemented: MovieWatchlistUseCase { + func contains(movie: Movie) -> Bool { + fatalError("Unimplemented") + } + + func add(movie: Movie) throws -> MovieWatchlist { + fatalError("Unimplemented") + } + + func remove(movie: Movie) throws -> MovieWatchlist { + fatalError("Unimplemented") + } + + func getWatchlist() throws -> MovieWatchlist { + fatalError("Unimplemented") + } + } + + @MainActor + public static var testValue: MovieWatchlistUseCase { + Unimplemented() + } +} + +public protocol MovieSeenlistUseCase { + func contains(movie: Movie) -> Bool + @discardableResult + func add(movie: Movie) throws -> MovieSeenList + @discardableResult + func remove(movie: Movie) throws -> MovieSeenList + func getSeenList() throws -> MovieSeenList +} + +public enum MovieSeenlistUseCaseDependencyKey: TestDependencyKey { + struct Unimplemented: MovieSeenlistUseCase { + func contains(movie: Movie) -> Bool { + fatalError("Unimplemented") + } + + func add(movie: Movie) throws -> MovieSeenList { + fatalError("Unimplemented") + } + + func remove(movie: Movie) throws -> MovieSeenList { + fatalError("Unimplemented") + } + + func getSeenList() throws -> MovieSeenList { + fatalError("Unimplemented") + } + } + + public static var testValue: MovieSeenlistUseCase { + Unimplemented() + } +} + +public extension DependencyValues { + var movieListUseCase: MovieListUseCase { + get { + self[MovieListUseCaseDependencyKey.self] + } + set { + self[MovieListUseCaseDependencyKey.self] = newValue + } + } + + var movieWatchlistUseCase: MovieWatchlistUseCase { + get { + self[MovieWatchlistUseCaseDependencyKey.self] + } + set { + self[MovieWatchlistUseCaseDependencyKey.self] = newValue + } + } + + var movieSeenlistUseCase: MovieSeenlistUseCase { + get { + self[MovieSeenlistUseCaseDependencyKey.self] + } + set { + self[MovieSeenlistUseCaseDependencyKey.self] = newValue + } + } +} + +public enum MovieListError: Error { + case listNotFound +} diff --git a/Projects/Domain/MoviesDomain/UseCases/MovieRecomendationUseCase.swift b/Projects/Domain/MoviesDomain/UseCases/MovieRecomendationUseCase.swift new file mode 100644 index 00000000..8f8f8a4a --- /dev/null +++ b/Projects/Domain/MoviesDomain/UseCases/MovieRecomendationUseCase.swift @@ -0,0 +1,34 @@ +import Dependencies +import XCTestDynamicOverlay +import SwiftUI + +public protocol MovieRecomendationUseCase { + func fetchSimilar(movieID: MovieID) async throws -> [Movie] + func fetchRecomended(movieID: MovieID) async throws -> [Movie] +} + +public enum MovieRecomendationUseCaseDependencyKey: TestDependencyKey { + struct Unimplemented: MovieRecomendationUseCase { + func fetchSimilar(movieID: MovieID) throws -> [Movie] { + fatalError("Unimplemented") + } + func fetchRecomended(movieID: MovieID) throws -> [Movie] { + fatalError("Unimplemented") + } + } + + public static var testValue: MovieRecomendationUseCase { + Unimplemented() + } +} + +public extension DependencyValues { + var movieRecomendationUseCase: MovieRecomendationUseCase { + get { + self[MovieRecomendationUseCaseDependencyKey.self] + } + set { + self[MovieRecomendationUseCaseDependencyKey.self] = newValue + } + } +} diff --git a/Projects/Domain/MoviesDomain/UseCases/MovieSearchUseCase.swift b/Projects/Domain/MoviesDomain/UseCases/MovieSearchUseCase.swift new file mode 100644 index 00000000..1a962b09 --- /dev/null +++ b/Projects/Domain/MoviesDomain/UseCases/MovieSearchUseCase.swift @@ -0,0 +1,37 @@ +import Dependencies +import XCTestDynamicOverlay + +public protocol MovieSearchUseCase { + func search(query: String, page: Int) async throws -> PageResult +} + +public enum MovieSearchUseCaseDependencyKey: TestDependencyKey { + public static var testValue: any MovieSearchUseCase { + struct Unimplemented: MovieSearchUseCase { + func search(query: String, page: Int) async throws -> PageResult { + unimplemented( + #function, + placeholder: PageResult( + page: 0, + results: [], + totalPages: 0, + totalResults: 0 + ) + ) + } + } + + return Unimplemented() + } +} + +extension DependencyValues { + public var movieSearchUseCase: any MovieSearchUseCase { + get { + self[MovieSearchUseCaseDependencyKey.self] + } + set { + self[MovieSearchUseCaseDependencyKey.self] = newValue + } + } +} diff --git a/Projects/Domain/MoviesDomain/UseCases/PersonDetailsUseCase.swift b/Projects/Domain/MoviesDomain/UseCases/PersonDetailsUseCase.swift new file mode 100644 index 00000000..0006f46a --- /dev/null +++ b/Projects/Domain/MoviesDomain/UseCases/PersonDetailsUseCase.swift @@ -0,0 +1,43 @@ +import Dependencies +import XCTestDynamicOverlay + +public protocol PersonDetailsUseCase { + func fetchPersonDetails(with id: PersonID) async throws -> PersonDetails +} + +public enum PersonDetailsUseCaseDependencyKey: TestDependencyKey { + struct Unimplemented: PersonDetailsUseCase { + func fetchPersonDetails(with id: PersonID) throws -> PersonDetails { + fatalError() + } + } + + public static var testValue: PersonDetailsUseCase { + Unimplemented() + } +} + +extension DependencyValues { + public var personDetailsUseCase: PersonDetailsUseCase { + get { + self[PersonDetailsUseCaseDependencyKey.self] + } + set { + self[PersonDetailsUseCaseDependencyKey.self] = newValue + } + } +} + +#if DEBUG +public final class MockPersonDetailsUseCase: PersonDetailsUseCase { + public var _fetchPersonDetails: (PersonID) -> PersonDetails = { _ in + unimplemented("_fetchPersonDetails", placeholder: .example) + } + + public init() {} + + public func fetchPersonDetails(with id: PersonID) async throws -> PersonDetails { + _fetchPersonDetails(id) + } +} +#endif diff --git a/Projects/Domain/Project.swift b/Projects/Domain/Project.swift new file mode 100644 index 00000000..ecc5dc08 --- /dev/null +++ b/Projects/Domain/Project.swift @@ -0,0 +1,4 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let project = ProjectName.Domain.project diff --git a/CleanArchitectureRxSwift/Info.plist b/Projects/Example/Info.plist similarity index 64% rename from CleanArchitectureRxSwift/Info.plist rename to Projects/Example/Info.plist index 38e98af2..ae9013b0 100644 --- a/CleanArchitectureRxSwift/Info.plist +++ b/Projects/Example/Info.plist @@ -2,8 +2,12 @@ + UILaunchStoryboardName + LaunchScreen + LSApplicationCategoryType + CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -20,10 +24,8 @@ 1 LSRequiresIPhoneOS - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main + NSHumanReadableCopyright + Copyright ©. All rights reserved. UIRequiredDeviceCapabilities armv7 @@ -34,5 +36,12 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + diff --git a/Projects/Example/Project.swift b/Projects/Example/Project.swift new file mode 100644 index 00000000..0f134ed1 --- /dev/null +++ b/Projects/Example/Project.swift @@ -0,0 +1,4 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let project = ProjectName.Example.project diff --git a/Projects/Example/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Projects/Example/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/Projects/Example/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Projects/Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..9221b9bb --- /dev/null +++ b/Projects/Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Example/Resources/Assets.xcassets/Contents.json b/Projects/Example/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Projects/Example/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CleanArchitectureRxSwift/Base.lproj/LaunchScreen.storyboard b/Projects/Example/Resources/LaunchScreen.storyboard similarity index 66% rename from CleanArchitectureRxSwift/Base.lproj/LaunchScreen.storyboard rename to Projects/Example/Resources/LaunchScreen.storyboard index fdf3f97d..865e9329 100644 --- a/CleanArchitectureRxSwift/Base.lproj/LaunchScreen.storyboard +++ b/Projects/Example/Resources/LaunchScreen.storyboard @@ -1,7 +1,8 @@ - + - + + @@ -9,14 +10,11 @@ - - - - - + + diff --git a/Projects/Example/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/Projects/Example/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Projects/Example/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Example/Sources/ContentView.swift b/Projects/Example/Sources/ContentView.swift new file mode 100644 index 00000000..77fbff8e --- /dev/null +++ b/Projects/Example/Sources/ContentView.swift @@ -0,0 +1,35 @@ +import Anime +import AnimeAPI +import AnimeDomain +import Movies +import MoviesAPI +import MoviesDB +import MoviesDomain +import SwiftUI + +@MainActor +struct ContentView: View { + @StateObject + private var coordinator = AppCoordinator() + + var body: some View { + TabView { + coordinator.makeMoviesView() + .tag(AppCoordinator.Tab.movies) + + coordinator.makeAnimeView() + .tag(AppCoordinator.Tab.anime) + + coordinator.makeWatchlistsView() + .tag(AppCoordinator.Tab.watchlist) + } + } + + private func tabbarItem(text: String, image: String) -> some View { + VStack { + Image(systemName: image) + .imageScale(.large) + Text(text) + } + } +} diff --git a/Projects/Example/Sources/Dependencies+App/AnimeDetailUseCase+App.swift b/Projects/Example/Sources/Dependencies+App/AnimeDetailUseCase+App.swift new file mode 100644 index 00000000..e79bfb75 --- /dev/null +++ b/Projects/Example/Sources/Dependencies+App/AnimeDetailUseCase+App.swift @@ -0,0 +1,9 @@ +import AnimeDomain +import AnimeAPI +import Dependencies + +extension AnimeDetailUseCaseDependencyKey: @retroactive DependencyKey { + public static var liveValue: AnimeDetailUseCase { + AnimeAPIClient.shared + } +} diff --git a/Projects/Example/Sources/Dependencies+App/DiscoverAnimeUseCase+App.swift b/Projects/Example/Sources/Dependencies+App/DiscoverAnimeUseCase+App.swift new file mode 100644 index 00000000..bc29c652 --- /dev/null +++ b/Projects/Example/Sources/Dependencies+App/DiscoverAnimeUseCase+App.swift @@ -0,0 +1,9 @@ +import AnimeAPI +import AnimeDomain +import Dependencies + +extension DiscoverAnimeUseCaseDependencyKey: @retroactive DependencyKey { + public static var liveValue: DiscoverAnimeUseCase { + AnimeAPIClient.shared + } +} diff --git a/Projects/Example/Sources/Dependencies+App/DiscoverMoviesUseCase+App.swift b/Projects/Example/Sources/Dependencies+App/DiscoverMoviesUseCase+App.swift new file mode 100644 index 00000000..0c777540 --- /dev/null +++ b/Projects/Example/Sources/Dependencies+App/DiscoverMoviesUseCase+App.swift @@ -0,0 +1,18 @@ +import MoviesDomain +import Dependencies +import MoviesAPI +import MoviesDB +import HTTPClient +import Foundation + + +extension DiscoverMoviesUseCaseDependencyKey: @retroactive DependencyKey { + public static var liveValue: any DiscoverMoviesUseCaseProtocol { + @Dependency(\.httpClient) var httpClient + + return DiscoverMoviesUseCase( + gateway: MoviesAPI.DiscoverMoviesGateway(client: httpClient), + repository: MoviesDB.DicoverMoviesRepository() + ) + } +} diff --git a/Projects/Example/Sources/Dependencies+App/HTTPClient+App.swift b/Projects/Example/Sources/Dependencies+App/HTTPClient+App.swift new file mode 100644 index 00000000..74e9a746 --- /dev/null +++ b/Projects/Example/Sources/Dependencies+App/HTTPClient+App.swift @@ -0,0 +1,38 @@ +import HTTPClient +import Dependencies +import Foundation + +extension HTTPClient: @retroactive DependencyKey { + public static var liveValue: HTTPClient { + HTTPClient( + session: URLSession(configuration: .ephemeral), + environment: HTTPClient.Environment( + schema: "https", + host: "api.themoviedb.org", + version: "3" + ), + urlComponentsInterceptor: APIKeyInterceptor() + ) + } +} + +extension DependencyValues { + var httpClient: HTTPClient { + get { + self[HTTPClient.self] + } + set { + self[HTTPClient.self] = newValue + } + } +} + +struct APIKeyInterceptor: URLComponentsInterceptor { + func modify(components: inout URLComponents) { + var queryItems = components.queryItems ?? [] + queryItems.append( + URLQueryItem(name: "api_key", value: "1d9b898a212ea52e283351e521e17871") + ) + components.queryItems = queryItems + } +} diff --git a/Projects/Example/Sources/Dependencies+App/MediaListsUseCase+App.swift b/Projects/Example/Sources/Dependencies+App/MediaListsUseCase+App.swift new file mode 100644 index 00000000..f043ec27 --- /dev/null +++ b/Projects/Example/Sources/Dependencies+App/MediaListsUseCase+App.swift @@ -0,0 +1,15 @@ +import AnimeDomain +import AnimeDB +import Dependencies + +extension MediaWatchlistUseCaseDependencyKey: @retroactive DependencyKey { + public static var liveValue: any MediaWatchlistUseCase { + SDMediaWatchlistRepository() + } +} + +extension MovieSeenlistUseCaseDependencyKey: @retroactive DependencyKey { + public static var liveValue: any MediaSeenlistUseCase { + SDMediaSeenlistRepository() + } +} diff --git a/Projects/Example/Sources/Dependencies+App/MovieCreditsUseCase+App.swift b/Projects/Example/Sources/Dependencies+App/MovieCreditsUseCase+App.swift new file mode 100644 index 00000000..29785e07 --- /dev/null +++ b/Projects/Example/Sources/Dependencies+App/MovieCreditsUseCase+App.swift @@ -0,0 +1,10 @@ +import MoviesAPI +import MoviesDomain +import Dependencies + +extension MovieCreditsUseCaseDependencyKey: @retroactive DependencyKey { + public static var liveValue: any MovieCreditsUseCase { + @Dependency(\.httpClient) var httpClient + return MovieCreditsGateway(client: httpClient) + } +} diff --git a/Projects/Example/Sources/Dependencies+App/MovieDetailsGateway+App.swift b/Projects/Example/Sources/Dependencies+App/MovieDetailsGateway+App.swift new file mode 100644 index 00000000..64c4d304 --- /dev/null +++ b/Projects/Example/Sources/Dependencies+App/MovieDetailsGateway+App.swift @@ -0,0 +1,10 @@ +import Dependencies +import MoviesAPI +import MoviesDomain + +extension MovieDetailUseCaseProtocolDependencyKey: @retroactive DependencyKey { + public static var liveValue: MovieDetailUseCaseProtocol { + @Dependency(\.httpClient) var httpClient + return MoviesAPI.MovieDetailsGateway(client: httpClient) + } +} diff --git a/Projects/Example/Sources/Dependencies+App/MovieListUseCase+App.swift b/Projects/Example/Sources/Dependencies+App/MovieListUseCase+App.swift new file mode 100644 index 00000000..424e025f --- /dev/null +++ b/Projects/Example/Sources/Dependencies+App/MovieListUseCase+App.swift @@ -0,0 +1,22 @@ +import Dependencies +import MoviesDB +import MoviesDomain + +extension MovieListUseCaseDependencyKey: @retroactive DependencyKey { + public static var liveValue: MovieListUseCase { + MovieListRepository() + } +} + +extension MovieWatchlistUseCaseDependencyKey: @retroactive DependencyKey { + @MainActor + public static var liveValue: MovieWatchlistUseCase { + SDMovieWatchlistRepository() // MovieWatchlistRepository() + } +} + +extension MovieSeenlistUseCaseDependencyKey: @retroactive DependencyKey { + public static var liveValue: MovieSeenlistUseCase { + SDMovieSeenlistRepository() //MovieSeenlistRepository() + } +} diff --git a/Projects/Example/Sources/Dependencies+App/MovieRecomendationUseCase+App.swift b/Projects/Example/Sources/Dependencies+App/MovieRecomendationUseCase+App.swift new file mode 100644 index 00000000..63f2f015 --- /dev/null +++ b/Projects/Example/Sources/Dependencies+App/MovieRecomendationUseCase+App.swift @@ -0,0 +1,10 @@ +import Dependencies +import MoviesAPI +import MoviesDomain + +extension MovieRecomendationUseCaseDependencyKey: @retroactive DependencyKey { + public static var liveValue: MovieRecomendationUseCase { + @Dependency(\.httpClient) var httpClient + return MovieRecomendationGateway(client: httpClient) + } +} diff --git a/Projects/Example/Sources/Dependencies+App/MovieSearchUseCase+App.swift b/Projects/Example/Sources/Dependencies+App/MovieSearchUseCase+App.swift new file mode 100644 index 00000000..d6ee400e --- /dev/null +++ b/Projects/Example/Sources/Dependencies+App/MovieSearchUseCase+App.swift @@ -0,0 +1,11 @@ +import MoviesDomain +import MoviesAPI +import Dependencies + +extension MovieSearchUseCaseDependencyKey: @retroactive DependencyKey { + public static var liveValue: any MovieSearchUseCase { + @Dependency(\.httpClient) var httpClient + + return SearchMoviesGateway(client: httpClient) + } +} diff --git a/Projects/Example/Sources/Dependencies+App/PersonDetailsUseCase+App.swift b/Projects/Example/Sources/Dependencies+App/PersonDetailsUseCase+App.swift new file mode 100644 index 00000000..f18731b0 --- /dev/null +++ b/Projects/Example/Sources/Dependencies+App/PersonDetailsUseCase+App.swift @@ -0,0 +1,10 @@ +import Dependencies +import MoviesAPI +import MoviesDomain + +extension PersonDetailsUseCaseDependencyKey: @retroactive DependencyKey { + public static var liveValue: PersonDetailsUseCase { + @Dependency(\.httpClient) var httpClient + return PersonDetailsGateway(client: httpClient) + } +} diff --git a/Projects/Example/Sources/ModernCleanArchitectureApp.swift b/Projects/Example/Sources/ModernCleanArchitectureApp.swift new file mode 100644 index 00000000..00f7e005 --- /dev/null +++ b/Projects/Example/Sources/ModernCleanArchitectureApp.swift @@ -0,0 +1,15 @@ +import SwiftUI +import MoviesDomain +import MoviesAPI +import Movies + +@main +struct ModernCleanArchitectureApp: App { + var body: some Scene { + WindowGroup { + ContentView() + .errorShowing() + .tint(.orange) + } + } +} diff --git a/Projects/Example/Sources/Navigation/Anime/AnimeNavigationView.swift b/Projects/Example/Sources/Navigation/Anime/AnimeNavigationView.swift new file mode 100644 index 00000000..a468da0b --- /dev/null +++ b/Projects/Example/Sources/Navigation/Anime/AnimeNavigationView.swift @@ -0,0 +1,62 @@ +import Anime +import AnimeDomain +import SwiftUI +import ComposableArchitecture + +@MainActor +struct AnimeNavigationView: View { + @Bindable + private var coordinator: AnimeCoordinator + private let store: StoreOf + + init(coordinator: AnimeCoordinator, store: StoreOf) { + self.store = store + self.coordinator = coordinator + } + + var body: some View { + NavigationStack(path: $coordinator.routes) { + AnimeListView(store: store) + .navigationDestination(for: AnimeCoordinator.Route.self) { route in + switch route { + case .animeDetail(let param): + AnimeDetailView(store: param.value) + case .animeList(let param): + AnimeListView(store: param.value) + } + } + } + } +} + +@MainActor +@Observable +final class AnimeCoordinator: Anime.AnimeCoordinator { + var routes: [Route] = [] + + func showDetails(for anime: DiscoverMedia) { + let viewModel = AnimeDetailViewModel(anime: anime, coordinator: self) + let store = Store( + initialState: AnimeDetailReducer.State(anime: anime), + reducer: { + AnimeDetailReducer(coordinator: self) + } + ) + routes.append(.animeDetail(RouteIdentifier(value: store, id: { _ in anime.id }))) + } + + func showMedia(for genre: String) { + let store = Store( + initialState: AnimeListReducer.State(genre: genre), + reducer: { + AnimeListReducer(coordinator: self) + } + ) + routes.append(.animeList(.init(value: store, id: { _ in genre }))) + } + + enum Route: Hashable { + case animeDetail(RouteIdentifier>) + case animeList(RouteIdentifier>) + } +} diff --git a/Projects/Example/Sources/Navigation/AppCoordinator.swift b/Projects/Example/Sources/Navigation/AppCoordinator.swift new file mode 100644 index 00000000..7ed26c7b --- /dev/null +++ b/Projects/Example/Sources/Navigation/AppCoordinator.swift @@ -0,0 +1,65 @@ +import Anime +import ComposableArchitecture +import Movies +import SwiftUI +import Watchlist + +@MainActor +final class AppCoordinator: ObservableObject { + @Published + var tab: Tab = .movies + + private lazy var moviesCoordinator = MoviesCoordinator() + private lazy var animeCoordinator = AnimeCoordinator() + private lazy var watchlistsCoordinator = WatchlistsCoordinator() + private lazy var moviesViewModel = MoviesViewModel(coordinator: moviesCoordinator) + private lazy var moviesListsViewModel = MoviesListsViewModel(coordinator: watchlistsCoordinator) + private lazy var animeListsViewModel = AnimeListsViewModel() + private lazy var animeListStore = Store( + initialState: AnimeListReducer.State(genre: nil), + reducer: { + AnimeListReducer(coordinator: animeCoordinator) + } + ) + + func makeMoviesView() -> some View { + MoviesNavigationView(coordinator: moviesCoordinator, viewModel: moviesViewModel) + .tabItem { + tabbarItem(text: "Movies", image: "film.stack") + } + } + + func makeAnimeView() -> some View { + AnimeNavigationView(coordinator: animeCoordinator, store: animeListStore) + .tabItem { + tabbarItem(text: "Anime", image: "sparkles.rectangle.stack") + } + } + + func makeWatchlistsView() -> some View { + WatchlistNavigationView( + coordinator: watchlistsCoordinator, + moviesViewModel: moviesListsViewModel, + animeViewModel: animeListsViewModel + ) + .tabItem { + tabbarItem(text: "My Lists", image: "heart.square.fill") + } + } + + private func tabbarItem(text: String, image: String) -> some View { + VStack { + Image(systemName: image) + .imageScale(.large) + Text(text) + } + } +} + +extension AppCoordinator { + enum Tab { + case movies + case anime + case watchlist + } +} diff --git a/Projects/Example/Sources/Navigation/Movies/MoviesNavigationView.swift b/Projects/Example/Sources/Navigation/Movies/MoviesNavigationView.swift new file mode 100644 index 00000000..83af4ecf --- /dev/null +++ b/Projects/Example/Sources/Navigation/Movies/MoviesNavigationView.swift @@ -0,0 +1,88 @@ +import Combine +import Watchlist +import Movies +import MoviesDomain +import SwiftUI + +@MainActor +struct MoviesNavigationView: View { + @Bindable + private var coordinator: MoviesCoordinator + private let viewModel: MoviesViewModel + + init(coordinator: MoviesCoordinator, viewModel: MoviesViewModel) { + self.coordinator = coordinator + self.viewModel = viewModel + } + + var body: some View { + NavigationStack(path: $coordinator.routes) { + MoviesListView(viewModel: viewModel) + .navigationTitle("Movies") + .navigationDestination(for: MoviesCoordinator.Route.self) { route in + switch route { + case .movieDetail(let route): + MovieDetailView(viewModel: route.value) + case .personDetail(let route): + PersonDetailsView(viewModel: route.value) + case .addMoiveToList(let route): + AddToCustomListView(viewModel: route.value) + } + } + } + } +} + +@MainActor +@Observable +final class MoviesCoordinator: Movies.MoviesCoordinator { + var routes: [Route] = [] + + init() { + self.routes = routes + } + + func showDetail(for movie: Movie) { + routes.append(.movieDetail(RouteIdentifier(value: MovieDetailViewModel(movie: movie, coordinator: self), id: \.movie.id))) + } + + func showDetail(for person: Person) { + let route = RouteIdentifier( + value: PersonDetailsViewViewModel(person: person), + id: \.person.id + ) + routes.append(.personDetail(route)) + } + + func showAddMovieToCustomList(for movie: Movie) { + let route = RouteIdentifier( + value: AddToCustomListViewModel(movie: movie), + id: \.movie.id + ) + routes.append(.addMoiveToList(route)) + } + + enum Route: Hashable { + case movieDetail(RouteIdentifier) + case personDetail(RouteIdentifier) + case addMoiveToList(RouteIdentifier) + } +} + +struct RouteIdentifier: Hashable { + let value: T + let id: AnyHashable + + init(value: T, id: (T) -> ID) { + self.value = value + self.id = AnyHashable(id(value)) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + static func == (lhs: RouteIdentifier, rhs: RouteIdentifier) -> Bool { + return lhs.id == rhs.id + } +} diff --git a/Projects/Example/Sources/Navigation/Watchlist/WatchlistNavigationView.swift b/Projects/Example/Sources/Navigation/Watchlist/WatchlistNavigationView.swift new file mode 100644 index 00000000..10d87f1a --- /dev/null +++ b/Projects/Example/Sources/Navigation/Watchlist/WatchlistNavigationView.swift @@ -0,0 +1,104 @@ +import Movies +import MoviesDomain +import SwiftUI +import SwiftUINavigation +import Watchlist + +struct WatchlistNavigationView: View { + @Bindable var coordinator: WatchlistsCoordinator + let moviesViewModel: MoviesListsViewModel + let animeViewModel: AnimeListsViewModel + + public init( + coordinator: WatchlistsCoordinator, + moviesViewModel: MoviesListsViewModel, + animeViewModel: AnimeListsViewModel + ) { + self.coordinator = coordinator + self.moviesViewModel = moviesViewModel + self.animeViewModel = animeViewModel + } + + var body: some View { + NavigationStack(path: $coordinator.routes) { + MediaListsView(moviesViewModel: moviesViewModel, animeViewModel: animeViewModel) + .navigationDestination(for: WatchlistsCoordinator.Route.self) { route in + switch route { + case .movieDetail(let route): + MovieDetailView(viewModel: route.value) + case .personDetail(let route): + PersonDetailsView(viewModel: route.value) + case .customList(let route): + CustomListView(movieList: route.value) + case .addToCustomList(let route): + AddToCustomListView(viewModel: route.value) + } + } + .sheet(item: $coordinator.modal) { modal in + switch modal { + case .createCustomList(let didCreateList): + NavigationView { + CreateCustomListView( + viewModel: CreateCustomListViewModel( + coordinator: coordinator, + didCreateList: didCreateList + ) + ) + } + } + } + } + } +} + +@Observable +final class WatchlistsCoordinator: Watchlist.MoviesListsCoordinator, Movies.MoviesCoordinator, Watchlist.CreateCustomListCoordinator { + var routes: [Route] = [] + + var modal: Modal? + + enum Route: Hashable { + case movieDetail(RouteIdentifier) + case personDetail(RouteIdentifier) + case customList(RouteIdentifier) + case addToCustomList(RouteIdentifier) + } + + enum Modal: Identifiable { + case createCustomList(() -> Void) + + var id: String { + switch self { + case .createCustomList: + return "createCustomList" + } + } + } + + func showDetail(for movie: Movie) { + let viewModel = MovieDetailViewModel(movie: movie, coordinator: self) + routes.append(.movieDetail(.init(value: viewModel, id: \.movie.id))) + } + + func showAddMovieToCustomList(for movie: Movie) { + let viewModel = AddToCustomListViewModel(movie: movie) + routes.append(.addToCustomList(.init(value: viewModel, id: \.movie.id))) + } + + func showDetail(for person: Person) { + let viewModel = PersonDetailsViewViewModel(person: person) + routes.append(.personDetail(.init(value: viewModel, id: \.person.id))) + } + + func showCustomList(_ customList: MovieList) { + routes.append(.customList(.init(value: customList, id: \.id))) + } + + func showCreatCustomList(didCreateList: @escaping () -> Void) { + modal = .createCustomList(didCreateList) + } + + func dismissCreateList() { + modal = nil + } +} diff --git a/Projects/Features/Anime/AnimeCoordinator.swift b/Projects/Features/Anime/AnimeCoordinator.swift new file mode 100644 index 00000000..2f6c8113 --- /dev/null +++ b/Projects/Features/Anime/AnimeCoordinator.swift @@ -0,0 +1,7 @@ +import AnimeDomain + +@MainActor +public protocol AnimeCoordinator { + func showDetails(for anime: DiscoverMedia) + func showMedia(for genre: String) +} diff --git a/Projects/Features/Anime/AnimeDetail/AnimeDetailReducer.swift b/Projects/Features/Anime/AnimeDetail/AnimeDetailReducer.swift new file mode 100644 index 00000000..ae9985bd --- /dev/null +++ b/Projects/Features/Anime/AnimeDetail/AnimeDetailReducer.swift @@ -0,0 +1,115 @@ +import AnimeDomain +import ComposableArchitecture +import Dependencies +import UI + +@Reducer +public struct AnimeDetailReducer { + @Dependency(\.animeDetailUseCase) + private var animeDetailUseCase: AnimeDetailUseCase + + @Dependency(\.mediaWatchlistUseCase) + private var mediaWatchlistUseCase: MediaWatchlistUseCase + + @Dependency(\.mediaSeenlistUseCase) + private var mediaSeenlistUseCase: MediaSeenlistUseCase + + @Dependency(\.errorToastCoordinator) + private var errorToast + + private let coordinator: AnimeCoordinator + + public init(coordinator: AnimeCoordinator) { + self.coordinator = coordinator + } + + public var body: some Reducer { + Scope(state: \.getDetail, action: \.getDetail) { + Fetch { animeId in + try await animeDetailUseCase.fetchBy(id: animeId) + } + .errorHandling() + } + Reduce { state, action in + switch action { + case .viewDidLoad: + state.isInWatchlist = mediaWatchlistUseCase.contains(media: state.anime) + state.isInSeenlist = mediaSeenlistUseCase.contains(media: state.anime) + return .send(.getDetail(.fetch(state.anime.id))) + case .addToWatchlist: + if state.isInWatchlist { + do { + try mediaWatchlistUseCase.remove(media: state.anime) + state.isInWatchlist = false + } catch { + errorToast.show() + } + } else { + do { + try mediaWatchlistUseCase.add(media: state.anime) + state.isInWatchlist = true + } catch { + errorToast.show() + } + } + return .none + case .addToSeenlist: + if state.isInSeenlist { + do { + try mediaSeenlistUseCase.remove(media: state.anime) + state.isInSeenlist = false + } catch { + errorToast.show() + } + } else { + do { + try mediaSeenlistUseCase.add(media: state.anime) + state.isInSeenlist = true + } catch { + errorToast.show() + } + } + return .none + case .didTapGenre(let genere): + return .run { @MainActor _ in + coordinator.showMedia(for: genere) + } + case .didTapMedia(let media): + return .run { @MainActor _ in + coordinator.showDetails(for: media) + } + default: + return .none + } + } + } + + @ObservableState + public struct State: Equatable { + var getDetail: Fetch.State = .notInitiated + var isInWatchlist = false + var isInSeenlist = false + let anime: DiscoverMedia + + public init(anime: DiscoverMedia) { + self.anime = anime + } + + var mediaDetail: MediaDetail? { + getDetail.fetched + } + + var isLoading: Bool { + getDetail.isFetching + } + } + + public enum Action: Equatable { + case getDetail(Fetch.Action) + case viewDidLoad + case addToWatchlist + case addToSeenlist + case didTapGenre(String) + case didTapMedia(DiscoverMedia) + } +} diff --git a/Projects/Features/Anime/AnimeDetail/AnimeDetailView.swift b/Projects/Features/Anime/AnimeDetail/AnimeDetailView.swift new file mode 100644 index 00000000..afebb9a6 --- /dev/null +++ b/Projects/Features/Anime/AnimeDetail/AnimeDetailView.swift @@ -0,0 +1,101 @@ +import AnimeDomain +import Dependencies +import SwiftUI +import UI +import ComposableArchitecture + +public struct AnimeDetailView: View { + private let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + Group { + if let detail = store.mediaDetail { + render(detail: detail) + } else { + ProgressView() + } + } + .navigationTitle(store.anime.title) + .onViewDidLoad { + store.send(.viewDidLoad) + } + } + + @ViewBuilder + private func render( + detail: MediaDetail + ) -> some View { + List { + Section { + AnimePosterRow( + detail: detail, + didTapGenre: { genre in + store.send(.didTapGenre(genre)) + } + ) + MediaButtonsRow( + isWatchlistSelected: store.isInWatchlist, + isSeenlistSelected: store.isInSeenlist, + isListSelected: false, + didTapWatchlist: { + store.send(.addToWatchlist) + }, + didTapSeenlist: { + store.send(.addToSeenlist) + }, + didTapList: { } + ) + MediaOverviewView(overview: detail.description) + } + if let trailerURL = detail.trailerURL { + Section { + VStack(alignment: .leading) { + Text("Trailer:") + .foregroundColor(.primary) + .font(.headline) + WebView(url: trailerURL) + .frame(height: 200) + } + } + } + Section { + if detail.characters.isEmpty == false { + MediaCrossLineItemsRow( + title: "Characters:", + posterSize: .small, + items: detail.characters.map { character in + MediaCrossLineItemsRow.Item( + id: "\(character.id)", + imageURL: character.image.flatMap(URL.init(string:)), + title: character.name ?? "", + subtitle: nil, + didTap: {} + ) + } + ) + if detail.recommendations.isEmpty == false { + MediaCrossLineItemsRow( + title: "Recomendations:", + posterSize: .medium, + items: detail.recommendations.map { media in + MediaCrossLineItemsRow.Item( + id: "\(media.id)", + imageURL: media.coverImageURL, + title: media.title, + subtitle: nil, + didTap: { + store.send(AnimeDetailReducer.Action.didTapMedia(media)) + } + ) + } + ) + } + } + } + } + } +} diff --git a/Projects/Features/Anime/AnimeDetail/AnimeDetailViewModel.swift b/Projects/Features/Anime/AnimeDetail/AnimeDetailViewModel.swift new file mode 100644 index 00000000..ff33564b --- /dev/null +++ b/Projects/Features/Anime/AnimeDetail/AnimeDetailViewModel.swift @@ -0,0 +1,101 @@ +import AnimeDomain +import Dependencies +import SwiftUI +import UI + +@MainActor +@Observable +public final class AnimeDetailViewModel { + struct Props { + var detail: MediaDetail? + var isInWatchlist = false + var isInSeenlist = false + } + + @ObservationIgnored + @Dependency(\.animeDetailUseCase) + private var animeDetailUseCase: AnimeDetailUseCase + + @ObservationIgnored + @Dependency(\.mediaWatchlistUseCase) + private var mediaWatchlistUseCase: MediaWatchlistUseCase + + @ObservationIgnored + @Dependency(\.mediaSeenlistUseCase) + private var mediaSeenlistUseCase: MediaSeenlistUseCase + + @ObservationIgnored + @Dependency(\.errorToastCoordinator) + private var errorToast + + @ObservationIgnored + private let coordinator: AnimeCoordinator + + @ObservationIgnored + public let anime: DiscoverMedia + + var props = Props() + + public init(anime: DiscoverMedia, coordinator: AnimeCoordinator) { + self.anime = anime + self.coordinator = coordinator + } + + @MainActor + func fetch() async { + do { + let detail = try await animeDetailUseCase.fetchBy(id: anime.id) + props.isInWatchlist = mediaWatchlistUseCase.contains(media: anime) + props.isInSeenlist = mediaSeenlistUseCase.contains(media: anime) + props.detail = detail + } catch { + errorToast.show() + } + } + + func addToWatchlist() { + if props.isInWatchlist { + do { + try mediaWatchlistUseCase.remove(media: anime) + props.isInWatchlist = false + } catch { + errorToast.show() + } + } else { + do { + try mediaWatchlistUseCase.add(media: anime) + props.isInWatchlist = true + } catch { + errorToast.show() + } + } + } + + func addToSeenlist() { + if props.isInSeenlist { + do { + try mediaSeenlistUseCase.remove(media: anime) + props.isInSeenlist = false + } catch { + errorToast.show() + } + } else { + do { + try mediaSeenlistUseCase.add(media: anime) + props.isInSeenlist = true + } catch { + errorToast.show() + } + } + } + + func didTapList() {} + + func didTap(genre: String) { + coordinator.showMedia(for: genre) + } + + func didTap(media: DiscoverMedia) { + coordinator.showDetails(for: media) + } +} diff --git a/Projects/Features/Anime/AnimeDetail/Views/AnimePosterRow.swift b/Projects/Features/Anime/AnimeDetail/Views/AnimePosterRow.swift new file mode 100644 index 00000000..3f02f4ad --- /dev/null +++ b/Projects/Features/Anime/AnimeDetail/Views/AnimePosterRow.swift @@ -0,0 +1,54 @@ +import AnimeDomain +import SwiftUI +import UI + +struct AnimePosterRow: View { + var detail: MediaDetail + + var didTapGenre: (String) -> Void + + var body: some View { + ZStack(alignment: .leading) { + BackdropImageView( + posterURL: detail.bannerImage.flatMap(URL.init(string:)), height: 250 + ) + .blur(radius: 30) + + VStack(alignment: .leading) { + HStack { + PosterImageView( + posterSize: .medium, + posterURL: detail.coverImage.flatMap(URL.init(string:)) + ) + VStack(alignment: .leading) { + if let startDate = detail.startDate { + Text("\(Calendar.current.component(.year, from: startDate)) • \(detail.duration) min • \(detail.type)") + .font(.subheadline) + .foregroundStyle(.white) + } + Text(detail.title) + .foregroundStyle(.white) + + HStack { + PopularityBadge(score: detail.averageScore) + .foregroundStyle(.white) + Text("Popularity: \(detail.popularity)") + .foregroundStyle(.white) + } + } + } + + ScrollView(.horizontal) { + HStack { + ForEach(detail.genres, id: \.self) { genre in + RoundedBadge(text: genre, color: .white) + .onTapGesture { + didTapGenre(genre) + } + } + } + } + } + } + } +} diff --git a/Projects/Features/Anime/AnimeList/AnimeListReducer.swift b/Projects/Features/Anime/AnimeList/AnimeListReducer.swift new file mode 100644 index 00000000..17ea411b --- /dev/null +++ b/Projects/Features/Anime/AnimeList/AnimeListReducer.swift @@ -0,0 +1,80 @@ +import AnimeDomain +import ComposableArchitecture +import Foundation +import UI + +@Reducer +public struct AnimeListReducer { + @Dependency(\.discoverAnimeUseCase) + private var useCase: DiscoverAnimeUseCase + + private let coordinator: AnimeCoordinator + + public init(coordinator: AnimeCoordinator) { + self.coordinator = coordinator + } + + public var body: some Reducer { + Scope(state: \.getMedia, action: \.getMedia) { + Fetch { params in + try await useCase.fetch( + page: 0, + perPage: 50, + filter: params.filter, + mediaType: params.mediaType, + genres: params.genre.map { [$0] } ?? nil + ) + } + .errorHandling() + } + Reduce { state, action in + switch action { + case .loadData: + let params = Params(filter: state.filter, mediaType: state.mediaType, genre: state.genre) + return .send(.getMedia(.fetch(params))) + case .getMedia(.response(let response)): + state.media = response.response + return .none + case .setFilter(let filter): + state.filter = filter + return .send(.loadData) + case .setMediaType(let mediaType): + state.mediaType = mediaType + return .send(.loadData) + case .didSelectMedia(let media): + return .run { @MainActor _ in + coordinator.showDetails(for: media) + } + default: + return .none + } + } + } + + public struct Params: Equatable { + var filter: DiscoverAnimeFilter + var mediaType: MediaType + var genre: String? + } + + @ObservableState + public struct State: Equatable { + let genre: String? + var filter: DiscoverAnimeFilter = .trending + var mediaType: MediaType = .anime + var media: [DiscoverMedia] = [] + var getMedia: Fetch, Params>.State = .notInitiated + + public init(genre: String?) { + self.genre = genre + } + } + + public enum Action: Equatable { + case setFilter(DiscoverAnimeFilter) + case setMediaType(MediaType) + case didSelectMedia(DiscoverMedia) + case loadData + case getMedia(Fetch, Params>.Action) + } +} diff --git a/Projects/Features/Anime/AnimeList/AnimeListView.swift b/Projects/Features/Anime/AnimeList/AnimeListView.swift new file mode 100644 index 00000000..518c13ec --- /dev/null +++ b/Projects/Features/Anime/AnimeList/AnimeListView.swift @@ -0,0 +1,101 @@ +import AnimeDomain +import ComposableArchitecture +import SwiftUI +import UI + +public struct AnimeListView: View { + let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + List { + ForEach(store.media, id: \.id) { anime in + MediaRow( + title: anime.title, + posterURL: anime.coverImageURL, + score: anime.averageScore, + releaseDate: anime.startDate, + overview: anime.description + ) + .onTapGesture { + store.send(.didSelectMedia(anime)) + } + } + } + .onAppear { + store.send(.loadData) + } + .listStyle(.plain) + .navigationTitle(store.genre ?? store.mediaType.name) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Menu("Media type", systemImage: store.mediaType.imageName) { + ForEach(MediaType.allCases, id: \.self) { type in + Button(action: { + store.send(.setMediaType(type)) + }) { + HStack { + Text(type.name) + if type == store.mediaType { + Image(systemName: "checkmark.circle.fill") + } + } + } + } + } + } + ToolbarItem(placement: .topBarTrailing) { + Menu("Sort", systemImage: "slider.horizontal.3") { + ForEach(DiscoverAnimeFilter.allCases, id: \.self) { filter in + Button(action: { + store.send(.setFilter(filter)) + }) { + HStack { + Text(filter.name) + if filter == store.filter { + Image(systemName: "checkmark.circle.fill") + } + } + } + } + } + } + } + } +} + +extension MediaType { + var name: String { + switch self { + case .anime: + "Anime" + case .manga: + "Manga" + } + } + + var imageName: String { + switch self { + case .anime: + "sparkles.rectangle.stack" + case .manga: + "sparkles.square.filled.on.square" + } + } +} + +extension DiscoverAnimeFilter { + var name: String { + switch self { + case .allTimePopular: + "Most popular" + case .trending: + "Treding" + case .topRated: + "Top rated" + } + } +} diff --git a/Projects/Features/Movies/MovieDetails/MovieDetailView.swift b/Projects/Features/Movies/MovieDetails/MovieDetailView.swift new file mode 100644 index 00000000..0e192f20 --- /dev/null +++ b/Projects/Features/Movies/MovieDetails/MovieDetailView.swift @@ -0,0 +1,124 @@ +import Dependencies +import MoviesDomain +import SwiftUI +import UI + +public struct MovieDetailView: View { + private let viewModel: MovieDetailViewModel + + public init(viewModel: MovieDetailViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + Group { + if let movieDetails = viewModel.props.details, let cast = viewModel.props.cast { + render(detail: movieDetails, cast: cast, recommended: viewModel.props.recommended, similar: viewModel.props.similar) + } else { + ProgressView() + } + } + .navigationTitle(viewModel.movie.title) + .task { + await viewModel.fetchDetails() + } + } + + @ViewBuilder + private func render( + detail: MovieDetail, + cast: MovieCast, + recommended: [Movie], + similar: [Movie] + ) -> some View { + List { + Section { + MoviePosterRow(detail: detail) + MediaButtonsRow( + isWatchlistSelected: viewModel.props.isInWatchlist, + isSeenlistSelected: viewModel.props.isInSeenlist, + isListSelected: viewModel.props.isInCustomList, + didTapWatchlist: viewModel.addToWatchlist, + didTapSeenlist: viewModel.addToSeenList, + didTapList: viewModel.didTapList + ) + MediaOverviewView(overview: detail.overview) + } + Section { + MovieKeywordsRow(keywords: detail.keywords?.keywords ?? []) + if cast.cast.isEmpty == false { + MediaCrossLineItemsRow( + title: "Cast:", + posterSize: .small, + items: cast.cast.map { person in + MediaCrossLineItemsRow.Item( + id: person.renderingId, + imageURL: person.profilePath.map(ImageSize.small.path(poster:)), + title: person.name, + subtitle: person.character ?? person.job ?? person.department ?? "", + didTap: { + viewModel.didTap(person: person) + } + ) + } + ) + } + if cast.crew.isEmpty == false { + MediaCrossLineItemsRow( + title: "Crew:", + posterSize: .small, + items: cast.crew.map { person in + MediaCrossLineItemsRow.Item( + id: person.renderingId, + imageURL: person.profilePath.map(ImageSize.small.path(poster:)), + title: person.name, + subtitle: person.character ?? person.job ?? person.department ?? "", + didTap: { + viewModel.didTap(person: person) + } + ) + } + ) + } + if recommended.isEmpty == false { + MediaCrossLineItemsRow( + title: "Recommended:", + posterSize: .medium, + items: recommended.map { movie in + MediaCrossLineItemsRow.Item( + id: "\(movie.id.rawValue)", + imageURL: movie.posterPath.map(ImageSize.medium.path(poster:)), + title: movie.title, + didTap: { + viewModel.didTap(movie: movie) + } + ) + } + ) + } + if similar.isEmpty == false { + MediaCrossLineItemsRow( + title: "Similar:", + posterSize: .medium, + items: similar.map { movie in + MediaCrossLineItemsRow.Item( + id: "\(movie.id.rawValue)", + imageURL: movie.posterPath.map(ImageSize.medium.path(poster:)), + title: movie.title, + didTap: { + viewModel.didTap(movie: movie) + } + ) + } + ) + } + } + } + } +} + +extension Person { + var renderingId: String { + "\(id)+\(department ?? "none")+\(job ?? "none") \(character ?? "")" + } +} diff --git a/Projects/Features/Movies/MovieDetails/MovieDetailViewModel.swift b/Projects/Features/Movies/MovieDetails/MovieDetailViewModel.swift new file mode 100644 index 00000000..7999f8f5 --- /dev/null +++ b/Projects/Features/Movies/MovieDetails/MovieDetailViewModel.swift @@ -0,0 +1,125 @@ +import Dependencies +import MoviesDomain +import SwiftUI +import UI + +@MainActor +@Observable +public final class MovieDetailViewModel { + struct Props: Equatable { + var details: MovieDetail? + var cast: MovieCast? + var isInWatchlist: Bool = false + var isInSeenlist: Bool = false + var isInCustomList: Bool = false + var recommended: [Movie] = [] + var similar: [Movie] = [] + var isLoading: Bool = true + } + + @ObservationIgnored + @Dependency(\.movieDetailUseCase) + private var movieDetailsUseCase + + @ObservationIgnored + @Dependency(\.movieCreditsUseCase) + private var movieCreditsUseCase + + @ObservationIgnored + @Dependency(\.movieRecomendationUseCase) + private var movieRecomendationUseCase + + @ObservationIgnored + @Dependency(\.movieWatchlistUseCase) + private var movieWatchlistUseCase: MovieWatchlistUseCase + + @ObservationIgnored + @Dependency(\.movieSeenlistUseCase) + private var movieSeenlistUseCase: MovieSeenlistUseCase + + @ObservationIgnored + private var coordinator: any MoviesCoordinator + + @ObservationIgnored + @Dependency(\.errorToastCoordinator) + private var errorToast + + private(set) var props = Props() + + public let movie: Movie + + public init(movie: Movie, coordinator: any MoviesCoordinator) { + self.movie = movie + self.coordinator = coordinator + } + + func fetchDetails() async { + guard shouldLoad else { + return + } + do { + props.isLoading = true + + props.isInWatchlist = movieWatchlistUseCase.contains(movie: movie) + props.isInSeenlist = movieSeenlistUseCase.contains(movie: movie) + + async let details = try await movieDetailsUseCase.fetchDetail(for: movie.id) + async let cast = try await movieCreditsUseCase.fetchCast(movieID: movie.id) + async let recommended = try await movieRecomendationUseCase.fetchRecomended(movieID: movie.id) + async let similar = try await movieRecomendationUseCase.fetchSimilar(movieID: movie.id) + + props.details = try await details + props.cast = try await cast + props.recommended = try await recommended + props.similar = try await similar + props.isLoading = false + } catch { + props.isLoading = false + errorToast.show() + } + } + + func didTap(movie: Movie) { + coordinator.showDetail(for: movie) + } + + func didTap(person: Person) { + coordinator.showDetail(for: person) + } + + func addToWatchlist() { + do { + if movieWatchlistUseCase.contains(movie: movie) { + try movieWatchlistUseCase.remove(movie: movie) + props.isInWatchlist = false + } else { + try movieWatchlistUseCase.add(movie: movie) + props.isInWatchlist = true + } + } catch { + errorToast.show() + } + } + + func addToSeenList() { + do { + if movieSeenlistUseCase.contains(movie: movie) { + try movieSeenlistUseCase.remove(movie: movie) + props.isInSeenlist = false + } else { + try movieSeenlistUseCase.add(movie: movie) + props.isInSeenlist = true + } + } catch { + errorToast.show() + } + } + + func didTapList() { + coordinator.showAddMovieToCustomList(for: movie) + } + + private var shouldLoad: Bool { + props.details == nil && props.cast == nil + } +} diff --git a/Projects/Features/Movies/MovieDetails/Views/CrosslineMoviesRow.swift b/Projects/Features/Movies/MovieDetails/Views/CrosslineMoviesRow.swift new file mode 100644 index 00000000..fe8c4ebe --- /dev/null +++ b/Projects/Features/Movies/MovieDetails/Views/CrosslineMoviesRow.swift @@ -0,0 +1,31 @@ +import MoviesDomain +import SwiftUI + +struct CrosslineMoviesRow: View { + let title: String + let movies: [Movie] + let onTap: (Movie) -> Void + + var body: some View { + VStack(alignment: .leading) { + Text(title) + .foregroundColor(.primary) + .font(.headline) + + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack { + ForEach(movies, id: \.id) { movie in + MediaPosterItemView( + posterURL: movie.posterPath.map(ImageSize.medium.path(poster:)), + title: movie.title + ) + .onTapGesture { + onTap(movie) + } + } + } + } + } + .padding(.vertical) + } +} diff --git a/Projects/Features/Movies/MovieDetails/Views/MediaImageCell.swift b/Projects/Features/Movies/MovieDetails/Views/MediaImageCell.swift new file mode 100644 index 00000000..20e22533 --- /dev/null +++ b/Projects/Features/Movies/MovieDetails/Views/MediaImageCell.swift @@ -0,0 +1,33 @@ +import MoviesDomain +import SwiftUI +import UI + +struct MediaImageCell: View { + let imageURL: URL + let title: String + let subtitle: String? + + var body: some View { + VStack(alignment: .center) { + PosterImageView( + posterSize: .small, + posterURL: imageURL + ) + + Text(title) + .font(.footnote) + .foregroundColor(.primary) + .lineLimit(1) + if let subtitle { + Text(subtitle) + .font(.footnote) + .foregroundColor(.secondary) + .lineLimit(1) + } + } + .frame(width: 100) + .contentShape(Rectangle()) + } +} + +// person.character ?? person.job ?? person.department ?? "" diff --git a/Projects/Features/Movies/MovieDetails/Views/MediaPosterItemView.swift b/Projects/Features/Movies/MovieDetails/Views/MediaPosterItemView.swift new file mode 100644 index 00000000..4f2888f4 --- /dev/null +++ b/Projects/Features/Movies/MovieDetails/Views/MediaPosterItemView.swift @@ -0,0 +1,24 @@ +import MoviesDomain +import SwiftUI +import UI + +struct MediaPosterItemView: View { + let posterURL: URL? + let title: String + + var body: some View { + VStack(alignment: .center) { + PosterImageView( + posterSize: .medium, + posterURL: posterURL + ) + + Text(title) + .font(.footnote) + .foregroundColor(.primary) + .lineLimit(1) + } + .frame(width: PosterStyle.Size.medium.width) + .contentShape(Rectangle()) + } +} diff --git a/Projects/Features/Movies/MovieDetails/Views/MovieKeywordsRow.swift b/Projects/Features/Movies/MovieDetails/Views/MovieKeywordsRow.swift new file mode 100644 index 00000000..57644b95 --- /dev/null +++ b/Projects/Features/Movies/MovieDetails/Views/MovieKeywordsRow.swift @@ -0,0 +1,29 @@ +// +import MoviesDomain +import SwiftUI +import UI + +struct MovieKeywordsRow: View { + var keywords: [Keyword] + var body: some View { + if keywords.isEmpty { + EmptyView() + } else { + VStack(alignment: .leading) { + Text("Keywords:") + .foregroundColor(.primary) + .font(.headline) + + ScrollView(.horizontal) { + HStack { + ForEach(keywords, id: \.id) { keyword in + RoundedBadge( + text: keyword.name, color: Color(uiColor: UIColor.tertiarySystemFill) + ) + } + } + } + } + } + } +} diff --git a/Projects/Features/Movies/MovieDetails/Views/MoviePosterRow.swift b/Projects/Features/Movies/MovieDetails/Views/MoviePosterRow.swift new file mode 100644 index 00000000..93d48e7b --- /dev/null +++ b/Projects/Features/Movies/MovieDetails/Views/MoviePosterRow.swift @@ -0,0 +1,48 @@ +import MoviesDomain +import SwiftUI +import UI + +struct MoviePosterRow: View { + var detail: MovieDetail + + var body: some View { + ZStack(alignment: .leading) { + BackdropImageView( + posterURL: ImageSize.medium.path(poster: detail.backdropPath ?? ""), + height: 250 + ) + .blur(radius: 30) + + VStack(alignment: .leading) { + HStack { + PosterImageView( + posterSize: .medium, + posterURL: ImageSize.medium.path(poster: detail.posterPath ?? "") + ) + VStack(alignment: .leading) { + Text("\(Calendar.current.component(.year, from: detail.releaseDate)) • \(detail.runtime) min • \(detail.status)") + .font(.subheadline) + .foregroundStyle(.white) + Text(detail.title) + .foregroundStyle(.white) + + HStack { + PopularityBadge(score: Int(detail.voteAverage * 10)) + .foregroundStyle(.white) + Text("\(detail.voteCount) ratings") + .foregroundStyle(.white) + } + } + } + + ScrollView(.horizontal) { + HStack { + ForEach(detail.genres ?? [], id: \.id) { genre in + RoundedBadge(text: genre.name, color: .white) + } + } + } + } + } + } +} diff --git a/Projects/Features/Movies/MovieImages/ImageSize.swift b/Projects/Features/Movies/MovieImages/ImageSize.swift new file mode 100644 index 00000000..6a1eeb33 --- /dev/null +++ b/Projects/Features/Movies/MovieImages/ImageSize.swift @@ -0,0 +1,12 @@ +import Foundation + +public enum ImageSize: String { + case small = "https://image.tmdb.org/t/p/w154/" + case medium = "https://image.tmdb.org/t/p/w500/" + case cast = "https://image.tmdb.org/t/p/w185/" + case original = "https://image.tmdb.org/t/p/original/" + + func path(poster: String) -> URL { + return URL(string: rawValue)!.appendingPathComponent(poster) + } +} diff --git a/Projects/Features/Movies/MoviesList/MoviesCoordinator.swift b/Projects/Features/Movies/MoviesList/MoviesCoordinator.swift new file mode 100644 index 00000000..b9d37b7d --- /dev/null +++ b/Projects/Features/Movies/MoviesList/MoviesCoordinator.swift @@ -0,0 +1,8 @@ +import MoviesDomain + +@MainActor +public protocol MoviesCoordinator { + func showDetail(for movie: Movie) + func showDetail(for person: Person) + func showAddMovieToCustomList(for movie: Movie) +} diff --git a/Projects/Features/Movies/MoviesList/MoviesListView.swift b/Projects/Features/Movies/MoviesList/MoviesListView.swift new file mode 100644 index 00000000..504bdeb6 --- /dev/null +++ b/Projects/Features/Movies/MoviesList/MoviesListView.swift @@ -0,0 +1,66 @@ +import Dependencies +import MoviesDomain +import SwiftUI +import UI + +public struct MoviesListView: View { + let viewModel: MoviesViewModel + + public init(viewModel: MoviesViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + List { + ForEach(viewModel.movies, id: \.id) { movie in + MediaRow( + title: movie.title, + posterURL: movie.posterPath.map(ImageSize.medium.path(poster:)), + score: Int(movie.voteAverage * 10), + releaseDate: movie.releaseDate, + overview: movie.overview + ) + .onTapGesture { + viewModel.didSelect(movie: movie) + } + } + } + .listStyle(.plain) + .toolbar(content: { + ToolbarItem(placement: .topBarTrailing) { + Menu("Filter Movies", systemImage: "slider.horizontal.3") { + ForEach(DiscoverMoviesRequest.allCases, id: \.self) { request in + Button(action: { + viewModel.filter(request: request) + }) { + HStack { + Text(request.name) + if request == viewModel.request { + Image(systemName: "checkmark.circle.fill") + } + } + } + } + } + } + }) + .task { + await viewModel.fetch() + } + } +} + +extension DiscoverMoviesRequest { + var name: String { + switch self { + case .nowPlaying: + "Now Playing" + case .popular: + "Popular" + case .topRated: + "Top Rated" + case .upcoming: + "Upcoming" + } + } +} diff --git a/Projects/Features/Movies/MoviesList/MoviesViewModel.swift b/Projects/Features/Movies/MoviesList/MoviesViewModel.swift new file mode 100644 index 00000000..ea7ecd89 --- /dev/null +++ b/Projects/Features/Movies/MoviesList/MoviesViewModel.swift @@ -0,0 +1,47 @@ +import Dependencies +import MoviesDomain +import SwiftUI +import UI + +@MainActor +@Observable public final class MoviesViewModel { + var movies: [Movie] = [] + + @ObservationIgnored + @Dependency(\.discoverMoviesUseCase) + private var useCase: DiscoverMoviesUseCaseProtocol + + @ObservationIgnored + @Dependency(\.errorToastCoordinator) + private var errorToast + + @ObservationIgnored + private let coordinator: MoviesCoordinator + + var request: DiscoverMoviesRequest + + public init(coordinator: MoviesCoordinator) { + self.request = .nowPlaying + self.coordinator = coordinator + } + + func fetch() async { + do { + let page = try await useCase.fetch(request: request, page: 1) + movies = page.results + } catch { + errorToast.show() + } + } + + func filter(request: DiscoverMoviesRequest) { + self.request = request + Task { + await fetch() + } + } + + func didSelect(movie: Movie) { + coordinator.showDetail(for: movie) + } +} diff --git a/Projects/Features/Movies/PersonDetails/PersonDetailsView.swift b/Projects/Features/Movies/PersonDetails/PersonDetailsView.swift new file mode 100644 index 00000000..bf6791bc --- /dev/null +++ b/Projects/Features/Movies/PersonDetails/PersonDetailsView.swift @@ -0,0 +1,103 @@ +import Dependencies +import MoviesDomain +import SwiftUI +import UI + +public struct PersonDetailsView: View { + let viewModel: PersonDetailsViewViewModel + + public init(viewModel: PersonDetailsViewViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + Group { + if let movieDetails = viewModel.props.details { + render(details: movieDetails) + } else { + ProgressView() + } + } + .task { + await viewModel.fetch() + } + .navigationTitle(viewModel.person.name) + } + + private func render(details: PersonDetails) -> some View { + List { + Section { + PersonBiographyView(personDetails: details) + BiographyView(details: details) + + VStack { + PersonImagesRow(images: details.images.profiles) + } + } + } + .listStyle(.insetGrouped) + } +} + +#if DEBUG +#Preview { + PersonDetailsView( + viewModel: withDependencies({ dependencies in + let mock = MockPersonDetailsUseCase() + mock._fetchPersonDetails = { _ in + PersonDetails.example + } + dependencies.personDetailsUseCase = mock + }, operation: { + PersonDetailsViewViewModel(person: .example) + }) + ) +} + +#endif + +struct BiographyView: View { + var details: PersonDetails + + @State + var isExpanded: Bool = false + + var body: some View { + VStack(alignment: .leading) { + Text("Biography") + .foregroundColor(.primary) + .font(.headline) + + Text(details.biography) + .font(.subheadline) + .foregroundColor(.secondary) + .lineLimit(isExpanded ? nil : 4) + Button(action: { + isExpanded.toggle() + }) { + Text(isExpanded ? "Less..." : "More...") + } + } + } +} + +struct PersonImagesRow: View { + let images: [PersonDetails.Image] + + var body: some View { + VStack(alignment: .leading) { + Text("Images") + .font(.headline) + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ForEach(images, id: \PersonDetails.Image.filePath) { image in + PosterImageView( + posterSize: .small, + posterURL: ImageSize.cast.path(poster: image.filePath) + ) + } + } + } + } + } +} diff --git a/Projects/Features/Movies/PersonDetails/PersonDetailsViewViewModel.swift b/Projects/Features/Movies/PersonDetails/PersonDetailsViewViewModel.swift new file mode 100644 index 00000000..c4730d63 --- /dev/null +++ b/Projects/Features/Movies/PersonDetails/PersonDetailsViewViewModel.swift @@ -0,0 +1,37 @@ +import SwiftUI +import Dependencies +import MoviesDomain +import UI + +@Observable +public final class PersonDetailsViewViewModel { + struct Props { + var details: PersonDetails? + } + + @ObservationIgnored + @Dependency(\.personDetailsUseCase) + var personDetailsUseCase: PersonDetailsUseCase + + @ObservationIgnored + @Dependency(\.errorToastCoordinator) + private var errorToast + + var props = Props() + + public let person: Person + + public init(person: Person) { + self.person = person + } + + @MainActor + func fetch() async { + do { + let personDetails = try await personDetailsUseCase.fetchPersonDetails(with: person.id) + props.details = personDetails + } catch { + errorToast.show() + } + } +} diff --git a/Projects/Features/Movies/PersonDetails/Views/PersonBiographyView.swift b/Projects/Features/Movies/PersonDetails/Views/PersonBiographyView.swift new file mode 100644 index 00000000..c2e6e654 --- /dev/null +++ b/Projects/Features/Movies/PersonDetails/Views/PersonBiographyView.swift @@ -0,0 +1,28 @@ +import SwiftUI +import MoviesDomain +import UI + +struct PersonBiographyView: View { + var personDetails: PersonDetails + + var body: some View { + HStack(alignment: .top) { + PosterImageView( + posterSize: .medium, + posterURL: ImageSize.cast.path(poster: personDetails.profilePath ?? "") + ) + VStack(alignment: .leading) { + Text("Known for") + .font(.headline) + Text(personDetails.knownForDepartment) + .font(.callout) + + if let placeOfBirth = personDetails.placeOfBirth { + Text("Born at") + .font(.headline) + Text(placeOfBirth) + } + } + } + } +} diff --git a/Projects/Features/Project.swift b/Projects/Features/Project.swift new file mode 100644 index 00000000..60e66ffc --- /dev/null +++ b/Projects/Features/Project.swift @@ -0,0 +1,4 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let project = ProjectName.Features.project diff --git a/Projects/Features/Watchlist/AddToCustomList/AddToCustomListView.swift b/Projects/Features/Watchlist/AddToCustomList/AddToCustomListView.swift new file mode 100644 index 00000000..a2974dd2 --- /dev/null +++ b/Projects/Features/Watchlist/AddToCustomList/AddToCustomListView.swift @@ -0,0 +1,80 @@ +import Dependencies +import MoviesDomain +import SwiftUI +import UI + +public struct AddToCustomListView: View { + @State + var viewModel: AddToCustomListViewModel + + public init(viewModel: AddToCustomListViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + List { + ForEach(viewModel.props.lists, id: \.id) { list in + MovieListRow(list: list) + .withCheckmark(isOn: viewModel.binding(for: list)) + } + } + .onAppear { + viewModel.fetch() + } + .navigationTitle(Text("Movie Lists")) + } +} + +@Observable +public final class AddToCustomListViewModel { + @ObservationIgnored + @Dependency(\.movieListUseCase) + private var movieListUseCase: any MovieListUseCase + + @ObservationIgnored + @Dependency(\.errorToastCoordinator) + private var errorToast + + @ObservationIgnored + public let movie: Movie + + var props = Props() + + public init(movie: Movie) { + self.movie = movie + } + + func fetch() { + do { + let lists = try movieListUseCase.getCustomLists() + self.props.lists = lists + } catch { + errorToast.show() + } + } + + func binding(for list: MovieList) -> Binding { + return Binding( + get: { + list.movies.contains(where: { $0.id == self.movie.id }) + }, + set: { isOn in + do { + if isOn { + try self.movieListUseCase.add(movie: self.movie, to: list) + } else { + try self.movieListUseCase.remove(movie: self.movie, from: list) + } + self.fetch() + } catch { + self.errorToast.show() + } + } + ) + } + + struct Props { + var lists: [MovieList] = [] + } +} + diff --git a/Projects/Features/Watchlist/CreatCustomList/CreatCustomListView.swift b/Projects/Features/Watchlist/CreatCustomList/CreatCustomListView.swift new file mode 100644 index 00000000..14468b7e --- /dev/null +++ b/Projects/Features/Watchlist/CreatCustomList/CreatCustomListView.swift @@ -0,0 +1,201 @@ +import Combine +import Dependencies +import MoviesDomain +import SwiftUI +import UI + +public struct CreateCustomListView: View { + @Bindable + var viewModel: CreateCustomListViewModel + + public init(viewModel: CreateCustomListViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + Form { + Section(header: Text("List information")) { + HStack { + Text("Name:") + TextField("Name your list", text: $viewModel.props.listName) + } + } + Section(header: Text("List cover")) { + if let selectedImage = viewModel.props.selectedImage { + BackdropImageView(posterURL: ImageSize.medium.path(poster: selectedImage.path)) + .frame(width: 280, height: 168) + + Button(action: viewModel.removeCover) { + Text("Remove cover") + .foregroundStyle(Color.red) + } + } else { + SearchImageRow( + imageQuery: $viewModel.props.imageNameQuery, + queryDidChange: viewModel.search(query:) + ) + if viewModel.props.images.isEmpty == false { + PostersRow(images: viewModel.props.images) { image in + viewModel.didSelect(image: image) + } + } + } + } + } + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(action: viewModel.didTapCancel, label: { + Text("Cancel") + }) + } + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button(action: viewModel.didTapCreate, label: { + Text("Create") + }) + .disabled(viewModel.props.canCreate == false) + } + } + .navigationTitle("New list") + } +} + +struct SearchImageRow: View { + var imageQuery: Binding + var queryDidChange: (String) -> Void + + var body: some View { + HStack { + Image(systemName: "magnifyingglass") + TextField("Search image", text: imageQuery) + .onChange(of: imageQuery.wrappedValue) { _, newValue in + queryDidChange(newValue) + } + } + } +} + +struct PostersRow: View { + var images: [MovieImage] + var didTapImage: (MovieImage) -> Void + + var body: some View { + ScrollView(.horizontal) { + HStack { + ForEach(images, id: \.path) { image in + BackdropImageView(posterURL: ImageSize.medium.path(poster: image.path)) + .frame(width: 280, height: 168) + .containerShape(Rectangle()) + .onTapGesture { + didTapImage(image) + } + } + } + } + } +} + +@MainActor +@Observable +public final class CreateCustomListViewModel { + var props = Props() + + @ObservationIgnored + @Dependency(\.movieListUseCase) + private var movieListUseCase: any MovieListUseCase + + @ObservationIgnored + @Dependency(\.movieSearchUseCase) + private var movieSearchUseCase: any MovieSearchUseCase + + @ObservationIgnored + @Dependency(\.errorToastCoordinator) + private var errorToast + + private let coordinator: CreateCustomListCoordinator + private let didCreateList: () -> Void + + @ObservationIgnored + private var searchTask: Task? + + @ObservationIgnored + private lazy var debouncer = Debouncer(delay: 0.5, work: self.debounced(query:)) + + public init(coordinator: CreateCustomListCoordinator, didCreateList: @escaping () -> Void) { + self.coordinator = coordinator + self.didCreateList = didCreateList + } + + struct Props { + var listName: String = "" + var imageNameQuery: String = "" + var selectedImage: MovieImage? + var images: [MovieImage] = [] + + var canCreate: Bool { + listName.isEmpty == false + } + } + + func didTapCreate() { + do { + try movieListUseCase.create(name: props.listName, imagePath: props.selectedImage?.path) + coordinator.dismissCreateList() + didCreateList() + } catch { + errorToast.show() + } + } + + func search(query: String) { + debouncer.trigger(query) + } + + func didSelect(image: MovieImage) { + props.selectedImage = image + } + + func removeCover() { + props.selectedImage = nil + } + + private func debounced(query: String) { + searchTask?.cancel() + searchTask = Task { @MainActor in + do { + let pageResult = try await movieSearchUseCase.search(query: query, page: 1) + self.props.images = pageResult.results.compactMap(\.backdropPath) + .map(MovieImage.init(path:)) + } catch { + self.props.images = [] + errorToast.show() + } + } + } + + func didTapCancel() { + coordinator.dismissCreateList() + } +} + +public protocol CreateCustomListCoordinator { + func dismissCreateList() +} + +final class Debouncer { + private let delay: TimeInterval + private let subject = PassthroughSubject() + private let cancel: Cancellable? + + init(delay: TimeInterval, work: @escaping (Value) -> Void) { + self.delay = delay + self.cancel = subject + .debounce(for: .seconds(delay), scheduler: DispatchQueue.main) + .sink(receiveValue: work) + } + + func trigger(_ value: Value) { + subject.send(value) + } +} diff --git a/Projects/Features/Watchlist/CustomListView/CustomListView.swift b/Projects/Features/Watchlist/CustomListView/CustomListView.swift new file mode 100644 index 00000000..3e540366 --- /dev/null +++ b/Projects/Features/Watchlist/CustomListView/CustomListView.swift @@ -0,0 +1,24 @@ +import SwiftUI +import MoviesDomain +import UI + +public struct CustomListView: View { + public let movieList: MovieList + + public init(movieList: MovieList) { + self.movieList = movieList + } + + public var body: some View { + List(movieList.movies, id: \.id) { movie in + MediaRow( + title: movie.title, + posterURL: movie.posterPath.map(ImageSize.medium.path(poster:)), + score: Int(movie.voteAverage * 10), + releaseDate: movie.releaseDate, + overview: movie.overview + ) + } + .navigationTitle(movieList.name) + } +} diff --git a/Projects/Features/Watchlist/Watchlists/AnimeWatchlist/AnimeListsView.swift b/Projects/Features/Watchlist/Watchlists/AnimeWatchlist/AnimeListsView.swift new file mode 100644 index 00000000..17bb6580 --- /dev/null +++ b/Projects/Features/Watchlist/Watchlists/AnimeWatchlist/AnimeListsView.swift @@ -0,0 +1,94 @@ +import AnimeDomain +import SwiftUI +import UI + +struct AnimeListsView: View { + @State + private var selection: Selection = .wathclist + + let viewModel: AnimeListsViewModel + + init(viewModel: AnimeListsViewModel) { + self.viewModel = viewModel + } + + var body: some View { + List { + Picker(selection: $selection, label: Text("")) { + Text("Wishlist") + .tag(Selection.wathclist) + + Text("Seenlist") + .tag(Selection.seenlist) + } + .pickerStyle(SegmentedPickerStyle()) + + switch selection { + case .wathclist: + renderWatchlist() + case .seenlist: + renderSeenlist() + } + } + .listStyle(GroupedListStyle()) + .onAppear { + viewModel.fetch() + } + } + + private func renderWatchlist() -> some View { + Section(header: Text("Watchlist")) { + if let watchlist = viewModel.props.watchlist, watchlist.media.isEmpty == false { + ForEach(watchlist.media, id: \.id) { media in + MediaRow( + title: media.title, + posterURL: media.coverImageURL, + score: media.averageScore, + releaseDate: media.startDate, + overview: media.description + ) + } + } else { + ContentUnavailableView( + label: { + Text("No movies in your watchlist.") + }, + description: { + Text("Add some movies to your watchlist to see them here.") + } + ) + } + } + } + + private func renderSeenlist() -> some View { + Section(header: Text("Seenlist")) { + if let seenlist = self.viewModel.props.seenlist, seenlist.media.isEmpty == false { + ForEach(seenlist.media, id: \.id) { media in + MediaRow( + title: media.title, + posterURL: media.coverImageURL, + score: media.averageScore, + releaseDate: media.startDate, + overview: media.description + ) + } + } else { + ContentUnavailableView( + label: { + Text("No movies in your seenlist.") + }, + description: { + Text("Add some movies to your seenlist to see them here.") + } + ) + } + } + + } + + enum Selection { + case wathclist + case seenlist + } +} diff --git a/Projects/Features/Watchlist/Watchlists/AnimeWatchlist/AnimeListsViewModel.swift b/Projects/Features/Watchlist/Watchlists/AnimeWatchlist/AnimeListsViewModel.swift new file mode 100644 index 00000000..05457c25 --- /dev/null +++ b/Projects/Features/Watchlist/Watchlists/AnimeWatchlist/AnimeListsViewModel.swift @@ -0,0 +1,37 @@ +import AnimeDomain +import Dependencies +import SwiftUI +import UI + +@Observable +public final class AnimeListsViewModel { + @ObservationIgnored + @Dependency(\.mediaSeenlistUseCase) + private var seenlistUseCase: MediaSeenlistUseCase + + @ObservationIgnored + @Dependency(\.mediaWatchlistUseCase) + private var watchlistUseCase: MediaWatchlistUseCase + + @ObservationIgnored + @Dependency(\.errorToastCoordinator) + private var errorToast + + var props = Props() + + public init() {} + + func fetch() { + do { + props.seenlist = try seenlistUseCase.getSeenList() + props.watchlist = try watchlistUseCase.getWatchlist() + } catch { + errorToast.show() + } + } + + struct Props { + var watchlist: MediaWatchlist? + var seenlist: MediaSeenlist? + } +} diff --git a/Projects/Features/Watchlist/Watchlists/MediaListsView.swift b/Projects/Features/Watchlist/Watchlists/MediaListsView.swift new file mode 100644 index 00000000..1069282b --- /dev/null +++ b/Projects/Features/Watchlist/Watchlists/MediaListsView.swift @@ -0,0 +1,141 @@ +// +// MediaListsView.swift +// Watchlist +// +// Created by Sergii Shulga on 14/01/2025. +// + +import SwiftUI + +public struct MediaListsView: View { + @State + private var selection: Selection = .movies + + let moviesViewModel: MoviesListsViewModel + let animeViewModel: AnimeListsViewModel + + public init(moviesViewModel: MoviesListsViewModel, animeViewModel: AnimeListsViewModel) { + self.moviesViewModel = moviesViewModel + self.animeViewModel = animeViewModel + } + + public var body: some View { +// TabView(selection: $selection) { // For some reason TabView does not work when navigating back from a navigation controller + HorizontalPageView( + selection: selection, + moviesView: { + MoviesListsView(viewModel: moviesViewModel) + }, + animeView: { + AnimeListsView(viewModel: animeViewModel) + } + ) +// .tabViewStyle(.page) + .navigationTitle(selection == .movies ? "My Movies" : "My Anime") + .toolbar(content: { + ToolbarItemGroup(placement: .primaryAction) { + Picker(selection: $selection.animation(), label: Text("")) { + Image(systemName: "film") + .tag(Selection.movies) + + Image(systemName: "sparkles") + .tag(Selection.anime) + } + .pickerStyle(.inline) + } + }) + } + + enum Selection { + case movies + case anime + } +} + +import UIKit + +struct HorizontalPageView: UIViewControllerRepresentable { + var selection: MediaListsView.Selection + var moviesView: () -> MoviesListsView + var animeView: () -> AnimeListsView + + init( + selection: MediaListsView.Selection, + moviesView: @escaping () -> MoviesListsView, + animeView: @escaping () -> AnimeListsView + ) { + self.selection = selection + self.moviesView = moviesView + self.animeView = animeView + } + + func makeCoordinator() -> Coordinator { + Coordinator( + firstVC: UIHostingController(rootView: moviesView()), + secondVC: UIHostingController(rootView: animeView()) + ) + } + + func makeUIViewController(context: Context) -> UIPageViewController { + let viewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal) + switch selection { + case .movies: + viewController.setViewControllers( + [context.coordinator.firstVC], + direction: .reverse, + animated: false + ) + case .anime: + viewController.setViewControllers( + [context.coordinator.secondVC], + direction: .forward, + animated: false + ) + } + return viewController + } + + func updateUIViewController(_ uiViewController: UIPageViewController, context: Context) { + switch selection { + case .movies: + uiViewController.setViewControllers( + [context.coordinator.firstVC], + direction: .reverse, + animated: true + ) + case .anime: + uiViewController.setViewControllers( + [context.coordinator.secondVC], + direction: .forward, + animated: true + ) + } + uiViewController.delegate = context.coordinator + uiViewController.dataSource = context.coordinator + } + + final class Coordinator: NSObject, UIPageViewControllerDelegate, UIPageViewControllerDataSource { + let firstVC: UIViewController + let secondVC: UIViewController + + func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { + if viewController === firstVC { + return nil + } + return firstVC + } + + func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { + if viewController === secondVC { + return nil + } + return secondVC + } + + init(firstVC: UIViewController, secondVC: UIViewController) { + self.firstVC = firstVC + self.secondVC = secondVC + super.init() + } + } +} diff --git a/Projects/Features/Watchlist/Watchlists/MoviesWatchlist/MoviesListsCoordinator.swift b/Projects/Features/Watchlist/Watchlists/MoviesWatchlist/MoviesListsCoordinator.swift new file mode 100644 index 00000000..ad1de3c9 --- /dev/null +++ b/Projects/Features/Watchlist/Watchlists/MoviesWatchlist/MoviesListsCoordinator.swift @@ -0,0 +1,7 @@ +import MoviesDomain + +public protocol MoviesListsCoordinator { + func showDetail(for movie: Movie) + func showCustomList(_ list: MovieList) + func showCreatCustomList(didCreateList: @escaping () -> Void) +} diff --git a/Projects/Features/Watchlist/Watchlists/MoviesWatchlist/MoviesListsView.swift b/Projects/Features/Watchlist/Watchlists/MoviesWatchlist/MoviesListsView.swift new file mode 100644 index 00000000..3456ffe0 --- /dev/null +++ b/Projects/Features/Watchlist/Watchlists/MoviesWatchlist/MoviesListsView.swift @@ -0,0 +1,149 @@ +import Dependencies +import MoviesDomain +import SwiftUI +import UI + +public struct MoviesListsView: View { + let viewModel: MoviesListsViewModel + + @State + private var selectedList: Selection = .wathclist + + public init(viewModel: MoviesListsViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + List { + renderCustomLists() + + Picker(selection: $selectedList, label: Text("")) { + Text("Wishlist") + .tag(Selection.wathclist) + + Text("Seenlist") + .tag(Selection.seenlist) + } + .pickerStyle(SegmentedPickerStyle()) + + switch selectedList { + case .wathclist: + renderWatchlist() + case .seenlist: + renderSeenlist() + } + } + .listStyle(GroupedListStyle()) + .onAppear { + viewModel.fetch() + } + } + + func renderCustomLists() -> some View { + Section(header: Text("Custom Lists")) { + Button(action: viewModel.didTapCreateCustomList) { + Text("Create custom list") + } + ForEach(viewModel.props.customLists, id: \.id) { customList in + MovieListRow(list: customList) + .onTapGesture { + viewModel.didTap(customList: customList) + } + } + } + } + + @ViewBuilder + func renderWatchlist() -> some View { + Section(header: Text("Watchlist")) { + if let watchlist = viewModel.props.watchlist, watchlist.movies.isEmpty == false { + ForEach(watchlist.movies, id: \.id) { movie in + MediaRow( + title: movie.title, + posterURL: movie.posterPath.map(ImageSize.medium.path(poster:)), + score: Int(movie.voteAverage * 10), + releaseDate: movie.releaseDate, + overview: movie.overview + ) + .onTapGesture { + viewModel.didTap(movie: movie) + } + } + } else { + ContentUnavailableView( + label: { + Text("No movies in your watchlist.") + }, + description: { + Text("Add some movies to your watchlist to see them here.") + } + ) + } + } + } + + @ViewBuilder + func renderSeenlist() -> some View { + Section(header: Text("Seenlist")) { + if let seenList = self.viewModel.props.seenList, seenList.movies.isEmpty == false { + ForEach(seenList.movies, id: \.id) { movie in + MediaRow( + title: movie.title, + posterURL: movie.posterPath.map(ImageSize.medium.path(poster:)), + score: Int(movie.voteAverage * 10), + releaseDate: movie.releaseDate, + overview: movie.overview + ) + .onTapGesture { + viewModel.didTap(movie: movie) + } + } + } else { + ContentUnavailableView( + label: { + Text("No movies in your seenlist.") + }, + description: { + Text("Add some movies to your seenlist to see them here.") + } + ) + } + } + } + + enum Selection { + case wathclist + case seenlist + } +} + +struct MovieListRow: View { + var list: MovieList + + var body: some View { + HStack { + BackdropImageView(posterURL: list.imagePath + .map(ImageSize.medium.path(poster:)) + ) + .frame(width: 70, height: 42) + Text(list.name) + .font(.headline) + Spacer() + Text("\(list.movies.count) movies") + .font(.caption) + } + .contentShape(Rectangle()) + } +} + +// TOOD: extract this type with real endpoints +public enum ImageSize: String { + case small = "https://image.tmdb.org/t/p/w154/" + case medium = "https://image.tmdb.org/t/p/w500/" + case cast = "https://image.tmdb.org/t/p/w185/" + case original = "https://image.tmdb.org/t/p/original/" + + func path(poster: String) -> URL { + return URL(string: rawValue)!.appendingPathComponent(poster) + } +} diff --git a/Projects/Features/Watchlist/Watchlists/MoviesWatchlist/MoviesListsViewModel.swift b/Projects/Features/Watchlist/Watchlists/MoviesWatchlist/MoviesListsViewModel.swift new file mode 100644 index 00000000..67f13725 --- /dev/null +++ b/Projects/Features/Watchlist/Watchlists/MoviesWatchlist/MoviesListsViewModel.swift @@ -0,0 +1,64 @@ +import Dependencies +import MoviesDomain +import SwiftUI +import UI + +@Observable +public final class MoviesListsViewModel { + @ObservationIgnored + @Dependency(\.movieListUseCase) + private var movieListUseCase: MovieListUseCase + + @ObservationIgnored + @Dependency(\.movieWatchlistUseCase) + private var movieWatchlistUseCase: MovieWatchlistUseCase + + @ObservationIgnored + @Dependency(\.movieSeenlistUseCase) + private var movieSeenlistUseCase: MovieSeenlistUseCase + + @ObservationIgnored + @Dependency(\.errorToastCoordinator) + private var errorToast + + @ObservationIgnored + private let coordinator: MoviesListsCoordinator + + var props = Props() + + public init(coordinator: MoviesListsCoordinator) { + self.coordinator = coordinator + } + + @MainActor + func fetch() { + do { + props.customLists = try movieListUseCase.getCustomLists() + props.watchlist = try movieWatchlistUseCase.getWatchlist() + props.seenList = try movieSeenlistUseCase.getSeenList() + } catch { + errorToast.show() + } + } + + func didTap(movie: Movie) { + coordinator.showDetail(for: movie) + } + + @MainActor + func didTapCreateCustomList() { + coordinator.showCreatCustomList { [weak self] in + self?.fetch() + } + } + + func didTap(customList: MovieList) { + coordinator.showCustomList(customList) + } + + struct Props { + var watchlist: MovieWatchlist? + var seenList: MovieSeenList? + var customLists: [MovieList] = [] + } +} diff --git a/Projects/Platform/AnimeAPI/AnimeAPIClient.swift b/Projects/Platform/AnimeAPI/AnimeAPIClient.swift new file mode 100644 index 00000000..ab9e2d1b --- /dev/null +++ b/Projects/Platform/AnimeAPI/AnimeAPIClient.swift @@ -0,0 +1,49 @@ +import Foundation +import Apollo +import AnimeDomain +import ApolloExtensions + +public final class AnimeAPIClient { + private let client = ApolloClient(url: URL(string: "https://graphql.anilist.co")!) + + init() { + } + + public static let shared = AnimeAPIClient() +} + +extension AnimeAPIClient: DiscoverAnimeUseCase { + public func fetch( + page: Int, + perPage: Int, + filter: AnimeDomain.DiscoverAnimeFilter, + mediaType: AnimeDomain.MediaType, + genres: [String]? + ) async throws -> AnimeDomain.Paged<[AnimeDomain.DiscoverMedia]> { + let query = PaginatedMediaQuery( + page: .some(page), + perPage: .some(page), + mediaSort: .some(filter.asMediaSort.map(GraphQLEnum.case)), + type: .some(.case(mediaType.asGQL)), + genreIn: genres.map { GraphQLNullable.some($0.map(Optional.some)) } ?? .null + ) + let result = try await client.fetch(query: query) + guard let page = result.page.flatMap(AnimeDomain.Paged.init(page:)) else { + throw NoDataError() + } + return page + } +} + +extension AnimeAPIClient: AnimeDetailUseCase { + public func fetchBy(id: Int) async throws -> MediaDetail { + let query = MediaByIdQuery(mediaId: .some(id)) + let result = try await client.fetch(query: query) + + guard let media = result.media.map(MediaDetail.make(with:)) else { + throw NoDataError() + } + + return media + } +} diff --git a/Projects/Platform/AnimeAPI/Entities/Date+GQL.swift b/Projects/Platform/AnimeAPI/Entities/Date+GQL.swift new file mode 100644 index 00000000..e7272c42 --- /dev/null +++ b/Projects/Platform/AnimeAPI/Entities/Date+GQL.swift @@ -0,0 +1,16 @@ +import Foundation + +extension FuzzyDateFragmet { + var date: Date? { + guard let year = self.year, let month = self.month, let day = self.day else { + return nil + } + return Calendar.current.date( + from: DateComponents( + year: year, + month: month, + day: day + ) + ) + } +} diff --git a/Projects/Platform/AnimeAPI/Entities/DiscoverAnime+GQL.swift b/Projects/Platform/AnimeAPI/Entities/DiscoverAnime+GQL.swift new file mode 100644 index 00000000..3c6ec06d --- /dev/null +++ b/Projects/Platform/AnimeAPI/Entities/DiscoverAnime+GQL.swift @@ -0,0 +1,29 @@ +import AnimeDomain +import Apollo +import Foundation + +extension DiscoverMedia { + init(media: GQLDiscoverMedia) { + let startDate = media.startDate?.fragments.fuzzyDateFragmet.date + let endDate = media.endDate?.fragments.fuzzyDateFragmet.date + + self.init( + id: media.id, + startDate: startDate, + endDate: endDate, + coverImageURL: media.coverImage?.large.flatMap(URL.init(string:)), + title: media.title?.english ?? "", + description: media.description ?? "", + averageScore: media.averageScore ?? 0 + ) + } +} + +extension Calendar { + func date(from fuzzyDate: FuzzyDateFragmet) -> Date? { + guard let year = fuzzyDate.year, let month = fuzzyDate.month, let day = fuzzyDate.day else { + return nil + } + return self.date(from: DateComponents(year: year, month: month, day: day)) + } +} diff --git a/Projects/Platform/AnimeAPI/Entities/DiscoverAnimeFilter+GQL.swift b/Projects/Platform/AnimeAPI/Entities/DiscoverAnimeFilter+GQL.swift new file mode 100644 index 00000000..1d4deb55 --- /dev/null +++ b/Projects/Platform/AnimeAPI/Entities/DiscoverAnimeFilter+GQL.swift @@ -0,0 +1,14 @@ +import AnimeDomain + +extension DiscoverAnimeFilter { + var asMediaSort: [MediaSort] { + switch self { + case .allTimePopular: + return [.popularityDesc] + case .trending: + return [.trendingDesc] + case .topRated: + return [.scoreDesc] + } + } +} diff --git a/Projects/Platform/AnimeAPI/Entities/MediaDetail+GQL.swift b/Projects/Platform/AnimeAPI/Entities/MediaDetail+GQL.swift new file mode 100644 index 00000000..f78281d3 --- /dev/null +++ b/Projects/Platform/AnimeAPI/Entities/MediaDetail+GQL.swift @@ -0,0 +1,48 @@ +import AnimeDomain +import Foundation + +extension MediaDetail { + static func make(with media: MediaByIdQuery.Data.Media) -> Self { + let recommendations = media.recommendations?.nodes? + .compactMap { $0?.mediaRecommendation?.fragments.gQLDiscoverMedia } + .map { + DiscoverMedia(media: $0) + } + + let trailerURL: URL? + + if media.trailer?.site == "youtube", let id = media.trailer?.id { + trailerURL = URL(string: "https://www.youtube.com/embed/\(id)") + } else { + trailerURL = nil + } + + return MediaDetail( + id: media.id, + coverImage: media.coverImage?.large, + trailerURL: trailerURL, + genres: media.genres?.compactMap { $0 } ?? [], + duration: media.duration ?? 0, + startDate: media.startDate?.fragments.fuzzyDateFragmet.date, + type: media.type?.rawValue ?? "", + popularity: media.popularity ?? 0, + averageScore: media.averageScore ?? 0, + description: media.description ?? "", + bannerImage: media.bannerImage, + characters: media.characters?.nodes?.compactMap { $0 } + .map(MediaDetail.Character.make(with:)) ?? [], + title: media.title?.english ?? "", + recommendations: recommendations ?? [] + ) + } +} + +extension MediaDetail.Character { + static func make(with character: MediaByIdQuery.Data.Media.Characters.Node) -> Self { + MediaDetail.Character( + id: character.id, + name: character.name?.full, + image: character.image?.medium + ) + } +} diff --git a/Projects/Platform/AnimeAPI/Entities/MediaType+GQL.swift b/Projects/Platform/AnimeAPI/Entities/MediaType+GQL.swift new file mode 100644 index 00000000..0cf953e6 --- /dev/null +++ b/Projects/Platform/AnimeAPI/Entities/MediaType+GQL.swift @@ -0,0 +1,12 @@ +import AnimeDomain + +extension AnimeDomain.MediaType { + var asGQL: AnimeAPI.MediaType { + switch self { + case .anime: + return .anime + case .manga: + return .manga + } + } +} diff --git a/Projects/Platform/AnimeAPI/Entities/PageInfo+GQL.swift b/Projects/Platform/AnimeAPI/Entities/PageInfo+GQL.swift new file mode 100644 index 00000000..c13043ec --- /dev/null +++ b/Projects/Platform/AnimeAPI/Entities/PageInfo+GQL.swift @@ -0,0 +1,8 @@ +import Apollo +import AnimeDomain + +extension PageInfo { + init(gqlPageInfo: PaginatedMediaQuery.Data.Page.PageInfo) { + self.init(currentPage: gqlPageInfo.currentPage ?? 0, hasNextPage: gqlPageInfo.hasNextPage ?? false) + } +} diff --git a/Projects/Platform/AnimeAPI/Entities/Paged+GQL.swift b/Projects/Platform/AnimeAPI/Entities/Paged+GQL.swift new file mode 100644 index 00000000..b8daf9b4 --- /dev/null +++ b/Projects/Platform/AnimeAPI/Entities/Paged+GQL.swift @@ -0,0 +1,17 @@ +import Apollo +import AnimeDomain +import Foundation + +extension Paged where T == [DiscoverMedia] { + init?(page: PaginatedMediaQuery.Data.Page) { + guard let pageInfo = page.pageInfo, let media = page.media else { + return nil + } + self.init( + response: media.compactMap { $0 }.map { + DiscoverMedia(media: $0.fragments.gQLDiscoverMedia) + }, + pageInfo: PageInfo(gqlPageInfo: pageInfo) + ) + } +} diff --git a/Projects/Platform/AnimeAPI/GraphQL/Fragments/FuzzyDate.graphql b/Projects/Platform/AnimeAPI/GraphQL/Fragments/FuzzyDate.graphql new file mode 100644 index 00000000..62c0b096 --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Fragments/FuzzyDate.graphql @@ -0,0 +1,5 @@ +fragment FuzzyDateFragmet on FuzzyDate { + year + month + day +} diff --git a/Projects/Platform/AnimeAPI/GraphQL/Fragments/GQLDiscoverMedia.graphql b/Projects/Platform/AnimeAPI/GraphQL/Fragments/GQLDiscoverMedia.graphql new file mode 100644 index 00000000..6fef4f4a --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Fragments/GQLDiscoverMedia.graphql @@ -0,0 +1,18 @@ +fragment GQLDiscoverMedia on Media { + id + startDate { + ...FuzzyDateFragmet + } + endDate { + ...FuzzyDateFragmet + } + coverImage { + large + } + title { + english + native + } + description + averageScore +} diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Fragments/FuzzyDateFragmet.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Fragments/FuzzyDateFragmet.graphql.swift new file mode 100644 index 00000000..b1f33af9 --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Fragments/FuzzyDateFragmet.graphql.swift @@ -0,0 +1,28 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public struct FuzzyDateFragmet: AnimeAPI.SelectionSet, Fragment { + public static var fragmentDefinition: StaticString { + #"fragment FuzzyDateFragmet on FuzzyDate { __typename year month day }"# + } + + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.FuzzyDate } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("year", Int?.self), + .field("month", Int?.self), + .field("day", Int?.self), + ] } + + /// Numeric Year (2017) + public var year: Int? { __data["year"] } + /// Numeric Month (3) + public var month: Int? { __data["month"] } + /// Numeric Day (24) + public var day: Int? { __data["day"] } +} diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Fragments/GQLDiscoverMedia.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Fragments/GQLDiscoverMedia.graphql.swift new file mode 100644 index 00000000..706fdaa3 --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Fragments/GQLDiscoverMedia.graphql.swift @@ -0,0 +1,133 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public struct GQLDiscoverMedia: AnimeAPI.SelectionSet, Fragment { + public static var fragmentDefinition: StaticString { + #"fragment GQLDiscoverMedia on Media { __typename id startDate { __typename ...FuzzyDateFragmet } endDate { __typename ...FuzzyDateFragmet } coverImage { __typename large } title { __typename english native } description averageScore }"# + } + + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.Media } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("id", Int.self), + .field("startDate", StartDate?.self), + .field("endDate", EndDate?.self), + .field("coverImage", CoverImage?.self), + .field("title", Title?.self), + .field("description", String?.self), + .field("averageScore", Int?.self), + ] } + + /// The id of the media + public var id: Int { __data["id"] } + /// The first official release date of the media + public var startDate: StartDate? { __data["startDate"] } + /// The last official release date of the media + public var endDate: EndDate? { __data["endDate"] } + /// The cover images of the media + public var coverImage: CoverImage? { __data["coverImage"] } + /// The official titles of the media in various languages + public var title: Title? { __data["title"] } + /// Short description of the media's story and characters + public var description: String? { __data["description"] } + /// A weighted average score of all the user's scores of the media + public var averageScore: Int? { __data["averageScore"] } + + /// StartDate + /// + /// Parent Type: `FuzzyDate` + public struct StartDate: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.FuzzyDate } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .fragment(FuzzyDateFragmet.self), + ] } + + /// Numeric Year (2017) + public var year: Int? { __data["year"] } + /// Numeric Month (3) + public var month: Int? { __data["month"] } + /// Numeric Day (24) + public var day: Int? { __data["day"] } + + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var fuzzyDateFragmet: FuzzyDateFragmet { _toFragment() } + } + } + + /// EndDate + /// + /// Parent Type: `FuzzyDate` + public struct EndDate: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.FuzzyDate } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .fragment(FuzzyDateFragmet.self), + ] } + + /// Numeric Year (2017) + public var year: Int? { __data["year"] } + /// Numeric Month (3) + public var month: Int? { __data["month"] } + /// Numeric Day (24) + public var day: Int? { __data["day"] } + + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var fuzzyDateFragmet: FuzzyDateFragmet { _toFragment() } + } + } + + /// CoverImage + /// + /// Parent Type: `MediaCoverImage` + public struct CoverImage: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.MediaCoverImage } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("large", String?.self), + ] } + + /// The cover image url of the media at a large size + public var large: String? { __data["large"] } + } + + /// Title + /// + /// Parent Type: `MediaTitle` + public struct Title: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.MediaTitle } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("english", String?.self), + .field("native", String?.self), + ] } + + /// The official english title + public var english: String? { __data["english"] } + /// Official title in it's native language + public var native: String? { __data["native"] } + } +} diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Operations/Queries/MediaByIdQuery.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Operations/Queries/MediaByIdQuery.graphql.swift new file mode 100644 index 00000000..673a6698 --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Operations/Queries/MediaByIdQuery.graphql.swift @@ -0,0 +1,369 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public class MediaByIdQuery: GraphQLQuery { + public static let operationName: String = "MediaById" + public static let operationDocument: ApolloAPI.OperationDocument = .init( + definition: .init( + #"query MediaById($mediaId: Int) { Media(id: $mediaId) { __typename id coverImage { __typename large } duration startDate { __typename ...FuzzyDateFragmet } genres popularity averageScore description bannerImage characters { __typename nodes { __typename image { __typename medium } name { __typename full } id } } type title { __typename english } trailer { __typename site id } recommendations(page: 0, perPage: 15) { __typename nodes { __typename mediaRecommendation { __typename ...GQLDiscoverMedia } } } } }"#, + fragments: [FuzzyDateFragmet.self, GQLDiscoverMedia.self] + )) + + public var mediaId: GraphQLNullable + + public init(mediaId: GraphQLNullable) { + self.mediaId = mediaId + } + + public var __variables: Variables? { ["mediaId": mediaId] } + + public struct Data: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.Query } + public static var __selections: [ApolloAPI.Selection] { [ + .field("Media", Media?.self, arguments: ["id": .variable("mediaId")]), + ] } + + /// Media query + public var media: Media? { __data["Media"] } + + /// Media + /// + /// Parent Type: `Media` + public struct Media: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.Media } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("id", Int.self), + .field("coverImage", CoverImage?.self), + .field("duration", Int?.self), + .field("startDate", StartDate?.self), + .field("genres", [String?]?.self), + .field("popularity", Int?.self), + .field("averageScore", Int?.self), + .field("description", String?.self), + .field("bannerImage", String?.self), + .field("characters", Characters?.self), + .field("type", GraphQLEnum?.self), + .field("title", Title?.self), + .field("trailer", Trailer?.self), + .field("recommendations", Recommendations?.self, arguments: [ + "page": 0, + "perPage": 15 + ]), + ] } + + /// The id of the media + public var id: Int { __data["id"] } + /// The cover images of the media + public var coverImage: CoverImage? { __data["coverImage"] } + /// The general length of each anime episode in minutes + public var duration: Int? { __data["duration"] } + /// The first official release date of the media + public var startDate: StartDate? { __data["startDate"] } + /// The genres of the media + public var genres: [String?]? { __data["genres"] } + /// The number of users with the media on their list + public var popularity: Int? { __data["popularity"] } + /// A weighted average score of all the user's scores of the media + public var averageScore: Int? { __data["averageScore"] } + /// Short description of the media's story and characters + public var description: String? { __data["description"] } + /// The banner image of the media + public var bannerImage: String? { __data["bannerImage"] } + /// The characters in the media + public var characters: Characters? { __data["characters"] } + /// The type of the media; anime or manga + public var type: GraphQLEnum? { __data["type"] } + /// The official titles of the media in various languages + public var title: Title? { __data["title"] } + /// Media trailer or advertisement + public var trailer: Trailer? { __data["trailer"] } + /// User recommendations for similar media + public var recommendations: Recommendations? { __data["recommendations"] } + + /// Media.CoverImage + /// + /// Parent Type: `MediaCoverImage` + public struct CoverImage: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.MediaCoverImage } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("large", String?.self), + ] } + + /// The cover image url of the media at a large size + public var large: String? { __data["large"] } + } + + /// Media.StartDate + /// + /// Parent Type: `FuzzyDate` + public struct StartDate: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.FuzzyDate } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .fragment(FuzzyDateFragmet.self), + ] } + + /// Numeric Year (2017) + public var year: Int? { __data["year"] } + /// Numeric Month (3) + public var month: Int? { __data["month"] } + /// Numeric Day (24) + public var day: Int? { __data["day"] } + + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var fuzzyDateFragmet: FuzzyDateFragmet { _toFragment() } + } + } + + /// Media.Characters + /// + /// Parent Type: `CharacterConnection` + public struct Characters: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.CharacterConnection } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("nodes", [Node?]?.self), + ] } + + public var nodes: [Node?]? { __data["nodes"] } + + /// Media.Characters.Node + /// + /// Parent Type: `Character` + public struct Node: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.Character } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("image", Image?.self), + .field("name", Name?.self), + .field("id", Int.self), + ] } + + /// Character images + public var image: Image? { __data["image"] } + /// The names of the character + public var name: Name? { __data["name"] } + /// The id of the character + public var id: Int { __data["id"] } + + /// Media.Characters.Node.Image + /// + /// Parent Type: `CharacterImage` + public struct Image: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.CharacterImage } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("medium", String?.self), + ] } + + /// The character's image of media at medium size + public var medium: String? { __data["medium"] } + } + + /// Media.Characters.Node.Name + /// + /// Parent Type: `CharacterName` + public struct Name: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.CharacterName } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("full", String?.self), + ] } + + /// The character's first and last name + public var full: String? { __data["full"] } + } + } + } + + /// Media.Title + /// + /// Parent Type: `MediaTitle` + public struct Title: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.MediaTitle } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("english", String?.self), + ] } + + /// The official english title + public var english: String? { __data["english"] } + } + + /// Media.Trailer + /// + /// Parent Type: `MediaTrailer` + public struct Trailer: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.MediaTrailer } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("site", String?.self), + .field("id", String?.self), + ] } + + /// The site the video is hosted by (Currently either youtube or dailymotion) + public var site: String? { __data["site"] } + /// The trailer video id + public var id: String? { __data["id"] } + } + + /// Media.Recommendations + /// + /// Parent Type: `RecommendationConnection` + public struct Recommendations: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.RecommendationConnection } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("nodes", [Node?]?.self), + ] } + + public var nodes: [Node?]? { __data["nodes"] } + + /// Media.Recommendations.Node + /// + /// Parent Type: `Recommendation` + public struct Node: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.Recommendation } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("mediaRecommendation", MediaRecommendation?.self), + ] } + + /// The recommended media + public var mediaRecommendation: MediaRecommendation? { __data["mediaRecommendation"] } + + /// Media.Recommendations.Node.MediaRecommendation + /// + /// Parent Type: `Media` + public struct MediaRecommendation: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.Media } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .fragment(GQLDiscoverMedia.self), + ] } + + /// The id of the media + public var id: Int { __data["id"] } + /// The first official release date of the media + public var startDate: StartDate? { __data["startDate"] } + /// The last official release date of the media + public var endDate: EndDate? { __data["endDate"] } + /// The cover images of the media + public var coverImage: CoverImage? { __data["coverImage"] } + /// The official titles of the media in various languages + public var title: Title? { __data["title"] } + /// Short description of the media's story and characters + public var description: String? { __data["description"] } + /// A weighted average score of all the user's scores of the media + public var averageScore: Int? { __data["averageScore"] } + + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var gQLDiscoverMedia: GQLDiscoverMedia { _toFragment() } + } + + /// Media.Recommendations.Node.MediaRecommendation.StartDate + /// + /// Parent Type: `FuzzyDate` + public struct StartDate: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.FuzzyDate } + + /// Numeric Year (2017) + public var year: Int? { __data["year"] } + /// Numeric Month (3) + public var month: Int? { __data["month"] } + /// Numeric Day (24) + public var day: Int? { __data["day"] } + + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var fuzzyDateFragmet: FuzzyDateFragmet { _toFragment() } + } + } + + /// Media.Recommendations.Node.MediaRecommendation.EndDate + /// + /// Parent Type: `FuzzyDate` + public struct EndDate: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.FuzzyDate } + + /// Numeric Year (2017) + public var year: Int? { __data["year"] } + /// Numeric Month (3) + public var month: Int? { __data["month"] } + /// Numeric Day (24) + public var day: Int? { __data["day"] } + + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var fuzzyDateFragmet: FuzzyDateFragmet { _toFragment() } + } + } + + public typealias CoverImage = GQLDiscoverMedia.CoverImage + + public typealias Title = GQLDiscoverMedia.Title + } + } + } + } + } +} diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Operations/Queries/PaginatedMediaQuery.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Operations/Queries/PaginatedMediaQuery.graphql.swift new file mode 100644 index 00000000..f1618ce9 --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Operations/Queries/PaginatedMediaQuery.graphql.swift @@ -0,0 +1,187 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public class PaginatedMediaQuery: GraphQLQuery { + public static let operationName: String = "PaginatedMedia" + public static let operationDocument: ApolloAPI.OperationDocument = .init( + definition: .init( + #"query PaginatedMedia($page: Int, $perPage: Int, $mediaSort: [MediaSort], $type: MediaType, $genreIn: [String]) { Page(page: $page, perPage: $perPage) { __typename media(sort: $mediaSort, type: $type, genre_in: $genreIn) { __typename ...GQLDiscoverMedia } pageInfo { __typename currentPage hasNextPage } } }"#, + fragments: [FuzzyDateFragmet.self, GQLDiscoverMedia.self] + )) + + public var page: GraphQLNullable + public var perPage: GraphQLNullable + public var mediaSort: GraphQLNullable<[GraphQLEnum?]> + public var type: GraphQLNullable> + public var genreIn: GraphQLNullable<[String?]> + + public init( + page: GraphQLNullable, + perPage: GraphQLNullable, + mediaSort: GraphQLNullable<[GraphQLEnum?]>, + type: GraphQLNullable>, + genreIn: GraphQLNullable<[String?]> + ) { + self.page = page + self.perPage = perPage + self.mediaSort = mediaSort + self.type = type + self.genreIn = genreIn + } + + public var __variables: Variables? { [ + "page": page, + "perPage": perPage, + "mediaSort": mediaSort, + "type": type, + "genreIn": genreIn + ] } + + public struct Data: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.Query } + public static var __selections: [ApolloAPI.Selection] { [ + .field("Page", Page?.self, arguments: [ + "page": .variable("page"), + "perPage": .variable("perPage") + ]), + ] } + + public var page: Page? { __data["Page"] } + + /// Page + /// + /// Parent Type: `Page` + public struct Page: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.Page } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("media", [Medium?]?.self, arguments: [ + "sort": .variable("mediaSort"), + "type": .variable("type"), + "genre_in": .variable("genreIn") + ]), + .field("pageInfo", PageInfo?.self), + ] } + + public var media: [Medium?]? { __data["media"] } + /// The pagination information + public var pageInfo: PageInfo? { __data["pageInfo"] } + + /// Page.Medium + /// + /// Parent Type: `Media` + public struct Medium: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.Media } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .fragment(GQLDiscoverMedia.self), + ] } + + /// The id of the media + public var id: Int { __data["id"] } + /// The first official release date of the media + public var startDate: StartDate? { __data["startDate"] } + /// The last official release date of the media + public var endDate: EndDate? { __data["endDate"] } + /// The cover images of the media + public var coverImage: CoverImage? { __data["coverImage"] } + /// The official titles of the media in various languages + public var title: Title? { __data["title"] } + /// Short description of the media's story and characters + public var description: String? { __data["description"] } + /// A weighted average score of all the user's scores of the media + public var averageScore: Int? { __data["averageScore"] } + + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var gQLDiscoverMedia: GQLDiscoverMedia { _toFragment() } + } + + /// Page.Medium.StartDate + /// + /// Parent Type: `FuzzyDate` + public struct StartDate: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.FuzzyDate } + + /// Numeric Year (2017) + public var year: Int? { __data["year"] } + /// Numeric Month (3) + public var month: Int? { __data["month"] } + /// Numeric Day (24) + public var day: Int? { __data["day"] } + + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var fuzzyDateFragmet: FuzzyDateFragmet { _toFragment() } + } + } + + /// Page.Medium.EndDate + /// + /// Parent Type: `FuzzyDate` + public struct EndDate: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.FuzzyDate } + + /// Numeric Year (2017) + public var year: Int? { __data["year"] } + /// Numeric Month (3) + public var month: Int? { __data["month"] } + /// Numeric Day (24) + public var day: Int? { __data["day"] } + + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var fuzzyDateFragmet: FuzzyDateFragmet { _toFragment() } + } + } + + public typealias CoverImage = GQLDiscoverMedia.CoverImage + + public typealias Title = GQLDiscoverMedia.Title + } + + /// Page.PageInfo + /// + /// Parent Type: `PageInfo` + public struct PageInfo: AnimeAPI.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { AnimeAPI.Objects.PageInfo } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("currentPage", Int?.self), + .field("hasNextPage", Bool?.self), + ] } + + /// The current page + public var currentPage: Int? { __data["currentPage"] } + /// If there is another page + public var hasNextPage: Bool? { __data["hasNextPage"] } + } + } + } +} diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Enums/MediaSort.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Enums/MediaSort.graphql.swift new file mode 100644 index 00000000..1def5ebf --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Enums/MediaSort.graphql.swift @@ -0,0 +1,45 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +/// Media sort enums +public enum MediaSort: String, EnumType { + case id = "ID" + case idDesc = "ID_DESC" + case titleRomaji = "TITLE_ROMAJI" + case titleRomajiDesc = "TITLE_ROMAJI_DESC" + case titleEnglish = "TITLE_ENGLISH" + case titleEnglishDesc = "TITLE_ENGLISH_DESC" + case titleNative = "TITLE_NATIVE" + case titleNativeDesc = "TITLE_NATIVE_DESC" + case type = "TYPE" + case typeDesc = "TYPE_DESC" + case format = "FORMAT" + case formatDesc = "FORMAT_DESC" + case startDate = "START_DATE" + case startDateDesc = "START_DATE_DESC" + case endDate = "END_DATE" + case endDateDesc = "END_DATE_DESC" + case score = "SCORE" + case scoreDesc = "SCORE_DESC" + case popularity = "POPULARITY" + case popularityDesc = "POPULARITY_DESC" + case trending = "TRENDING" + case trendingDesc = "TRENDING_DESC" + case episodes = "EPISODES" + case episodesDesc = "EPISODES_DESC" + case duration = "DURATION" + case durationDesc = "DURATION_DESC" + case status = "STATUS" + case statusDesc = "STATUS_DESC" + case chapters = "CHAPTERS" + case chaptersDesc = "CHAPTERS_DESC" + case volumes = "VOLUMES" + case volumesDesc = "VOLUMES_DESC" + case updatedAt = "UPDATED_AT" + case updatedAtDesc = "UPDATED_AT_DESC" + case searchMatch = "SEARCH_MATCH" + case favourites = "FAVOURITES" + case favouritesDesc = "FAVOURITES_DESC" +} diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Enums/MediaType.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Enums/MediaType.graphql.swift new file mode 100644 index 00000000..5ab53996 --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Enums/MediaType.graphql.swift @@ -0,0 +1,12 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +/// Media type enum, anime or manga. +public enum MediaType: String, EnumType { + /// Japanese Anime + case anime = "ANIME" + /// Asian comic + case manga = "MANGA" +} diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/Character.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/Character.graphql.swift new file mode 100644 index 00000000..0aa34282 --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/Character.graphql.swift @@ -0,0 +1,12 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + /// A character that features in an anime or manga + static let Character = ApolloAPI.Object( + typename: "Character", + implementedInterfaces: [] + ) +} \ No newline at end of file diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/CharacterConnection.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/CharacterConnection.graphql.swift new file mode 100644 index 00000000..0231b689 --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/CharacterConnection.graphql.swift @@ -0,0 +1,11 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + static let CharacterConnection = ApolloAPI.Object( + typename: "CharacterConnection", + implementedInterfaces: [] + ) +} \ No newline at end of file diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/CharacterImage.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/CharacterImage.graphql.swift new file mode 100644 index 00000000..2446a55a --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/CharacterImage.graphql.swift @@ -0,0 +1,11 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + static let CharacterImage = ApolloAPI.Object( + typename: "CharacterImage", + implementedInterfaces: [] + ) +} \ No newline at end of file diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/CharacterName.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/CharacterName.graphql.swift new file mode 100644 index 00000000..f4bb72dc --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/CharacterName.graphql.swift @@ -0,0 +1,12 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + /// The names of the character + static let CharacterName = ApolloAPI.Object( + typename: "CharacterName", + implementedInterfaces: [] + ) +} \ No newline at end of file diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/FuzzyDate.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/FuzzyDate.graphql.swift new file mode 100644 index 00000000..1dab302f --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/FuzzyDate.graphql.swift @@ -0,0 +1,12 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + /// Date object that allows for incomplete date values (fuzzy) + static let FuzzyDate = ApolloAPI.Object( + typename: "FuzzyDate", + implementedInterfaces: [] + ) +} \ No newline at end of file diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/Media.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/Media.graphql.swift new file mode 100644 index 00000000..263df67c --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/Media.graphql.swift @@ -0,0 +1,12 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + /// Anime or Manga + static let Media = ApolloAPI.Object( + typename: "Media", + implementedInterfaces: [] + ) +} \ No newline at end of file diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/MediaCoverImage.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/MediaCoverImage.graphql.swift new file mode 100644 index 00000000..d57751d1 --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/MediaCoverImage.graphql.swift @@ -0,0 +1,11 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + static let MediaCoverImage = ApolloAPI.Object( + typename: "MediaCoverImage", + implementedInterfaces: [] + ) +} \ No newline at end of file diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/MediaTitle.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/MediaTitle.graphql.swift new file mode 100644 index 00000000..ec41fbde --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/MediaTitle.graphql.swift @@ -0,0 +1,12 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + /// The official titles of the media in various languages + static let MediaTitle = ApolloAPI.Object( + typename: "MediaTitle", + implementedInterfaces: [] + ) +} \ No newline at end of file diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/MediaTrailer.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/MediaTrailer.graphql.swift new file mode 100644 index 00000000..19808408 --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/MediaTrailer.graphql.swift @@ -0,0 +1,12 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + /// Media trailer or advertisement + static let MediaTrailer = ApolloAPI.Object( + typename: "MediaTrailer", + implementedInterfaces: [] + ) +} \ No newline at end of file diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/Page.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/Page.graphql.swift new file mode 100644 index 00000000..1cef5718 --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/Page.graphql.swift @@ -0,0 +1,12 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + /// Page of data + static let Page = ApolloAPI.Object( + typename: "Page", + implementedInterfaces: [] + ) +} \ No newline at end of file diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/PageInfo.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/PageInfo.graphql.swift new file mode 100644 index 00000000..5fc4a29f --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/PageInfo.graphql.swift @@ -0,0 +1,11 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + static let PageInfo = ApolloAPI.Object( + typename: "PageInfo", + implementedInterfaces: [] + ) +} \ No newline at end of file diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/Query.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/Query.graphql.swift new file mode 100644 index 00000000..a4155e4b --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/Query.graphql.swift @@ -0,0 +1,11 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + static let Query = ApolloAPI.Object( + typename: "Query", + implementedInterfaces: [] + ) +} \ No newline at end of file diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/Recommendation.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/Recommendation.graphql.swift new file mode 100644 index 00000000..4864df46 --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/Recommendation.graphql.swift @@ -0,0 +1,12 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + /// Media recommendation + static let Recommendation = ApolloAPI.Object( + typename: "Recommendation", + implementedInterfaces: [] + ) +} \ No newline at end of file diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/RecommendationConnection.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/RecommendationConnection.graphql.swift new file mode 100644 index 00000000..7c3f1dcc --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/Objects/RecommendationConnection.graphql.swift @@ -0,0 +1,11 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + static let RecommendationConnection = ApolloAPI.Object( + typename: "RecommendationConnection", + implementedInterfaces: [] + ) +} \ No newline at end of file diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/SchemaConfiguration.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/SchemaConfiguration.swift new file mode 100644 index 00000000..87235012 --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/SchemaConfiguration.swift @@ -0,0 +1,15 @@ +// @generated +// This file was automatically generated and can be edited to +// provide custom configuration for a generated GraphQL schema. +// +// Any changes to this file will not be overwritten by future +// code generation execution. + +import ApolloAPI + +public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration { + public static func cacheKeyInfo(for type: ApolloAPI.Object, object: ApolloAPI.ObjectData) -> CacheKeyInfo? { + // Implement this function to configure cache key resolution for your schema types. + return nil + } +} diff --git a/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/SchemaMetadata.graphql.swift b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/SchemaMetadata.graphql.swift new file mode 100644 index 00000000..829d603f --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Generated/Schema/SchemaMetadata.graphql.swift @@ -0,0 +1,44 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public protocol SelectionSet: ApolloAPI.SelectionSet & ApolloAPI.RootSelectionSet +where Schema == AnimeAPI.SchemaMetadata {} + +public protocol InlineFragment: ApolloAPI.SelectionSet & ApolloAPI.InlineFragment +where Schema == AnimeAPI.SchemaMetadata {} + +public protocol MutableSelectionSet: ApolloAPI.MutableRootSelectionSet +where Schema == AnimeAPI.SchemaMetadata {} + +public protocol MutableInlineFragment: ApolloAPI.MutableSelectionSet & ApolloAPI.InlineFragment +where Schema == AnimeAPI.SchemaMetadata {} + +public enum SchemaMetadata: ApolloAPI.SchemaMetadata { + public static let configuration: ApolloAPI.SchemaConfiguration.Type = SchemaConfiguration.self + + public static func objectType(forTypename typename: String) -> ApolloAPI.Object? { + switch typename { + case "Query": return AnimeAPI.Objects.Query + case "Page": return AnimeAPI.Objects.Page + case "Media": return AnimeAPI.Objects.Media + case "FuzzyDate": return AnimeAPI.Objects.FuzzyDate + case "MediaCoverImage": return AnimeAPI.Objects.MediaCoverImage + case "MediaTitle": return AnimeAPI.Objects.MediaTitle + case "PageInfo": return AnimeAPI.Objects.PageInfo + case "CharacterConnection": return AnimeAPI.Objects.CharacterConnection + case "Character": return AnimeAPI.Objects.Character + case "CharacterImage": return AnimeAPI.Objects.CharacterImage + case "CharacterName": return AnimeAPI.Objects.CharacterName + case "MediaTrailer": return AnimeAPI.Objects.MediaTrailer + case "RecommendationConnection": return AnimeAPI.Objects.RecommendationConnection + case "Recommendation": return AnimeAPI.Objects.Recommendation + default: return nil + } + } +} + +public enum Objects {} +public enum Interfaces {} +public enum Unions {} diff --git a/Projects/Platform/AnimeAPI/GraphQL/Queries/MediaById.graphql b/Projects/Platform/AnimeAPI/GraphQL/Queries/MediaById.graphql new file mode 100644 index 00000000..44467c16 --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Queries/MediaById.graphql @@ -0,0 +1,43 @@ +query MediaById($mediaId: Int) { + Media(id: $mediaId) { + id + coverImage { + large + } + duration + startDate { + ...FuzzyDateFragmet + } + genres + popularity + averageScore + description + bannerImage + characters { + nodes { + image { + medium + } + name { + full + } + id + } + } + type + title { + english + } + trailer { + site + id + } + recommendations(page: 0, perPage: 15) { + nodes { + mediaRecommendation { + ...GQLDiscoverMedia + } + } + } + } +} diff --git a/Projects/Platform/AnimeAPI/GraphQL/Queries/PaginatedMedia.graphql b/Projects/Platform/AnimeAPI/GraphQL/Queries/PaginatedMedia.graphql new file mode 100644 index 00000000..d505c9c8 --- /dev/null +++ b/Projects/Platform/AnimeAPI/GraphQL/Queries/PaginatedMedia.graphql @@ -0,0 +1,11 @@ +query PaginatedMedia($page: Int, $perPage: Int, $mediaSort: [MediaSort], $type: MediaType, , $genreIn: [String]) { + Page(page: $page, perPage: $perPage) { + media(sort: $mediaSort, type: $type, genre_in: $genreIn) { + ...GQLDiscoverMedia + } + pageInfo { + currentPage + hasNextPage + } + } +} diff --git a/Projects/Platform/AnimeAPI/schema.graphqls b/Projects/Platform/AnimeAPI/schema.graphqls new file mode 100644 index 00000000..b769efa4 --- /dev/null +++ b/Projects/Platform/AnimeAPI/schema.graphqls @@ -0,0 +1,6451 @@ +type Query { + Page( + """The page number""" + page: Int + + """The amount of entries per page, max 50""" + perPage: Int + ): Page + + """Media query""" + Media( + """Filter by the media id""" + id: Int + + """Filter by the media's MyAnimeList id""" + idMal: Int + + """Filter by the start date of the media""" + startDate: FuzzyDateInt + + """Filter by the end date of the media""" + endDate: FuzzyDateInt + + """Filter by the season the media was released in""" + season: MediaSeason + + """ + The year of the season (Winter 2017 would also include December 2016 releases). Requires season argument + """ + seasonYear: Int + + """Filter by the media's type""" + type: MediaType + + """Filter by the media's format""" + format: MediaFormat + + """Filter by the media's current release status""" + status: MediaStatus + + """Filter by amount of episodes the media has""" + episodes: Int + + """Filter by the media's episode length""" + duration: Int + + """Filter by the media's chapter count""" + chapters: Int + + """Filter by the media's volume count""" + volumes: Int + + """Filter by if the media's intended for 18+ adult audiences""" + isAdult: Boolean + + """Filter by the media's genres""" + genre: String + + """Filter by the media's tags""" + tag: String + + """ + Only apply the tags filter argument to tags above this rank. Default: 18 + """ + minimumTagRank: Int + + """Filter by the media's tags with in a tag category""" + tagCategory: String + + """Filter by the media on the authenticated user's lists""" + onList: Boolean + + """Filter media by sites name with a online streaming or reading license""" + licensedBy: String + + """Filter media by sites id with a online streaming or reading license""" + licensedById: Int + + """Filter by the media's average score""" + averageScore: Int + + """Filter by the number of users with this media on their list""" + popularity: Int + + """Filter by the source type of the media""" + source: MediaSource + + """Filter by the media's country of origin""" + countryOfOrigin: CountryCode + + """If the media is officially licensed or a self-published doujin release""" + isLicensed: Boolean + + """Filter by search query""" + search: String + + """Filter by the media id""" + id_not: Int + + """Filter by the media id""" + id_in: [Int] + + """Filter by the media id""" + id_not_in: [Int] + + """Filter by the media's MyAnimeList id""" + idMal_not: Int + + """Filter by the media's MyAnimeList id""" + idMal_in: [Int] + + """Filter by the media's MyAnimeList id""" + idMal_not_in: [Int] + + """Filter by the start date of the media""" + startDate_greater: FuzzyDateInt + + """Filter by the start date of the media""" + startDate_lesser: FuzzyDateInt + + """Filter by the start date of the media""" + startDate_like: String + + """Filter by the end date of the media""" + endDate_greater: FuzzyDateInt + + """Filter by the end date of the media""" + endDate_lesser: FuzzyDateInt + + """Filter by the end date of the media""" + endDate_like: String + + """Filter by the media's format""" + format_in: [MediaFormat] + + """Filter by the media's format""" + format_not: MediaFormat + + """Filter by the media's format""" + format_not_in: [MediaFormat] + + """Filter by the media's current release status""" + status_in: [MediaStatus] + + """Filter by the media's current release status""" + status_not: MediaStatus + + """Filter by the media's current release status""" + status_not_in: [MediaStatus] + + """Filter by amount of episodes the media has""" + episodes_greater: Int + + """Filter by amount of episodes the media has""" + episodes_lesser: Int + + """Filter by the media's episode length""" + duration_greater: Int + + """Filter by the media's episode length""" + duration_lesser: Int + + """Filter by the media's chapter count""" + chapters_greater: Int + + """Filter by the media's chapter count""" + chapters_lesser: Int + + """Filter by the media's volume count""" + volumes_greater: Int + + """Filter by the media's volume count""" + volumes_lesser: Int + + """Filter by the media's genres""" + genre_in: [String] + + """Filter by the media's genres""" + genre_not_in: [String] + + """Filter by the media's tags""" + tag_in: [String] + + """Filter by the media's tags""" + tag_not_in: [String] + + """Filter by the media's tags with in a tag category""" + tagCategory_in: [String] + + """Filter by the media's tags with in a tag category""" + tagCategory_not_in: [String] + + """Filter media by sites name with a online streaming or reading license""" + licensedBy_in: [String] + + """Filter media by sites id with a online streaming or reading license""" + licensedById_in: [Int] + + """Filter by the media's average score""" + averageScore_not: Int + + """Filter by the media's average score""" + averageScore_greater: Int + + """Filter by the media's average score""" + averageScore_lesser: Int + + """Filter by the number of users with this media on their list""" + popularity_not: Int + + """Filter by the number of users with this media on their list""" + popularity_greater: Int + + """Filter by the number of users with this media on their list""" + popularity_lesser: Int + + """Filter by the source type of the media""" + source_in: [MediaSource] + + """The order the results will be returned in""" + sort: [MediaSort] + ): Media + + """Media Trend query""" + MediaTrend( + """Filter by the media id""" + mediaId: Int + + """Filter by date""" + date: Int + + """Filter by trending amount""" + trending: Int + + """Filter by score""" + averageScore: Int + + """Filter by popularity""" + popularity: Int + + """Filter by episode number""" + episode: Int + + """Filter to stats recorded while the media was releasing""" + releasing: Boolean + + """Filter by the media id""" + mediaId_not: Int + + """Filter by the media id""" + mediaId_in: [Int] + + """Filter by the media id""" + mediaId_not_in: [Int] + + """Filter by date""" + date_greater: Int + + """Filter by date""" + date_lesser: Int + + """Filter by trending amount""" + trending_greater: Int + + """Filter by trending amount""" + trending_lesser: Int + + """Filter by trending amount""" + trending_not: Int + + """Filter by score""" + averageScore_greater: Int + + """Filter by score""" + averageScore_lesser: Int + + """Filter by score""" + averageScore_not: Int + + """Filter by popularity""" + popularity_greater: Int + + """Filter by popularity""" + popularity_lesser: Int + + """Filter by popularity""" + popularity_not: Int + + """Filter by episode number""" + episode_greater: Int + + """Filter by episode number""" + episode_lesser: Int + + """Filter by episode number""" + episode_not: Int + + """The order the results will be returned in""" + sort: [MediaTrendSort] + ): MediaTrend + + """Airing schedule query""" + AiringSchedule( + """Filter by the id of the airing schedule item""" + id: Int + + """Filter by the id of associated media""" + mediaId: Int + + """Filter by the airing episode number""" + episode: Int + + """Filter by the time of airing""" + airingAt: Int + + """Filter to episodes that haven't yet aired""" + notYetAired: Boolean + + """Filter by the id of the airing schedule item""" + id_not: Int + + """Filter by the id of the airing schedule item""" + id_in: [Int] + + """Filter by the id of the airing schedule item""" + id_not_in: [Int] + + """Filter by the id of associated media""" + mediaId_not: Int + + """Filter by the id of associated media""" + mediaId_in: [Int] + + """Filter by the id of associated media""" + mediaId_not_in: [Int] + + """Filter by the airing episode number""" + episode_not: Int + + """Filter by the airing episode number""" + episode_in: [Int] + + """Filter by the airing episode number""" + episode_not_in: [Int] + + """Filter by the airing episode number""" + episode_greater: Int + + """Filter by the airing episode number""" + episode_lesser: Int + + """Filter by the time of airing""" + airingAt_greater: Int + + """Filter by the time of airing""" + airingAt_lesser: Int + + """The order the results will be returned in""" + sort: [AiringSort] + ): AiringSchedule + + """Character query""" + Character( + """Filter by character id""" + id: Int + + """Filter by character by if its their birthday today""" + isBirthday: Boolean + + """Filter by search query""" + search: String + + """Filter by character id""" + id_not: Int + + """Filter by character id""" + id_in: [Int] + + """Filter by character id""" + id_not_in: [Int] + + """The order the results will be returned in""" + sort: [CharacterSort] + ): Character + + """Staff query""" + Staff( + """Filter by the staff id""" + id: Int + + """Filter by staff by if its their birthday today""" + isBirthday: Boolean + + """Filter by search query""" + search: String + + """Filter by the staff id""" + id_not: Int + + """Filter by the staff id""" + id_in: [Int] + + """Filter by the staff id""" + id_not_in: [Int] + + """The order the results will be returned in""" + sort: [StaffSort] + ): Staff + + """Media list query""" + MediaList( + """Filter by a list entry's id""" + id: Int + + """Filter by a user's id""" + userId: Int + + """Filter by a user's name""" + userName: String + + """Filter by the list entries media type""" + type: MediaType + + """Filter by the watching/reading status""" + status: MediaListStatus + + """Filter by the media id of the list entry""" + mediaId: Int + + """ + Filter list entries to users who are being followed by the authenticated user + """ + isFollowing: Boolean + + """Filter by note words and #tags""" + notes: String + + """Filter by the date the user started the media""" + startedAt: FuzzyDateInt + + """Filter by the date the user completed the media""" + completedAt: FuzzyDateInt + + """ + Limit to only entries also on the auth user's list. Requires user id or name arguments. + """ + compareWithAuthList: Boolean + + """Filter by a user's id""" + userId_in: [Int] + + """Filter by the watching/reading status""" + status_in: [MediaListStatus] + + """Filter by the watching/reading status""" + status_not_in: [MediaListStatus] + + """Filter by the watching/reading status""" + status_not: MediaListStatus + + """Filter by the media id of the list entry""" + mediaId_in: [Int] + + """Filter by the media id of the list entry""" + mediaId_not_in: [Int] + + """Filter by note words and #tags""" + notes_like: String + + """Filter by the date the user started the media""" + startedAt_greater: FuzzyDateInt + + """Filter by the date the user started the media""" + startedAt_lesser: FuzzyDateInt + + """Filter by the date the user started the media""" + startedAt_like: String + + """Filter by the date the user completed the media""" + completedAt_greater: FuzzyDateInt + + """Filter by the date the user completed the media""" + completedAt_lesser: FuzzyDateInt + + """Filter by the date the user completed the media""" + completedAt_like: String + + """The order the results will be returned in""" + sort: [MediaListSort] + ): MediaList + + """ + Media list collection query, provides list pre-grouped by status & custom lists. User ID and Media Type arguments required. + """ + MediaListCollection( + """Filter by a user's id""" + userId: Int + + """Filter by a user's name""" + userName: String + + """Filter by the list entries media type""" + type: MediaType + + """Filter by the watching/reading status""" + status: MediaListStatus + + """Filter by note words and #tags""" + notes: String + + """Filter by the date the user started the media""" + startedAt: FuzzyDateInt + + """Filter by the date the user completed the media""" + completedAt: FuzzyDateInt + + """ + Always return completed list entries in one group, overriding the user's split completed option. + """ + forceSingleCompletedList: Boolean + + """Which chunk of list entries to load""" + chunk: Int + + """The amount of entries per chunk, max 500""" + perChunk: Int + + """Filter by the watching/reading status""" + status_in: [MediaListStatus] + + """Filter by the watching/reading status""" + status_not_in: [MediaListStatus] + + """Filter by the watching/reading status""" + status_not: MediaListStatus + + """Filter by note words and #tags""" + notes_like: String + + """Filter by the date the user started the media""" + startedAt_greater: FuzzyDateInt + + """Filter by the date the user started the media""" + startedAt_lesser: FuzzyDateInt + + """Filter by the date the user started the media""" + startedAt_like: String + + """Filter by the date the user completed the media""" + completedAt_greater: FuzzyDateInt + + """Filter by the date the user completed the media""" + completedAt_lesser: FuzzyDateInt + + """Filter by the date the user completed the media""" + completedAt_like: String + + """The order the results will be returned in""" + sort: [MediaListSort] + ): MediaListCollection + + """Collection of all the possible media genres""" + GenreCollection: [String] + + """Collection of all the possible media tags""" + MediaTagCollection( + """Mod Only""" + status: Int + ): [MediaTag] + + """User query""" + User( + """Filter by the user id""" + id: Int + + """Filter by the name of the user""" + name: String + + """Filter to moderators only if true""" + isModerator: Boolean + + """Filter by search query""" + search: String + + """The order the results will be returned in""" + sort: [UserSort] + ): User + + """Get the currently authenticated user""" + Viewer: User + + """Notification query""" + Notification( + """Filter by the type of notifications""" + type: NotificationType + + """Reset the unread notification count to 0 on load""" + resetNotificationCount: Boolean + + """Filter by the type of notifications""" + type_in: [NotificationType] + ): NotificationUnion + + """Studio query""" + Studio( + """Filter by the studio id""" + id: Int + + """Filter by search query""" + search: String + + """Filter by the studio id""" + id_not: Int + + """Filter by the studio id""" + id_in: [Int] + + """Filter by the studio id""" + id_not_in: [Int] + + """The order the results will be returned in""" + sort: [StudioSort] + ): Studio + + """Review query""" + Review( + """Filter by Review id""" + id: Int + + """Filter by media id""" + mediaId: Int + + """Filter by user id""" + userId: Int + + """Filter by media type""" + mediaType: MediaType + + """The order the results will be returned in""" + sort: [ReviewSort] + ): Review + + """Activity query""" + Activity( + """Filter by the activity id""" + id: Int + + """Filter by the owner user id""" + userId: Int + + """Filter by the id of the user who sent a message""" + messengerId: Int + + """Filter by the associated media id of the activity""" + mediaId: Int + + """Filter by the type of activity""" + type: ActivityType + + """ + Filter activity to users who are being followed by the authenticated user + """ + isFollowing: Boolean + + """Filter activity to only activity with replies""" + hasReplies: Boolean + + """Filter activity to only activity with replies or is of type text""" + hasRepliesOrTypeText: Boolean + + """Filter by the time the activity was created""" + createdAt: Int + + """Filter by the activity id""" + id_not: Int + + """Filter by the activity id""" + id_in: [Int] + + """Filter by the activity id""" + id_not_in: [Int] + + """Filter by the owner user id""" + userId_not: Int + + """Filter by the owner user id""" + userId_in: [Int] + + """Filter by the owner user id""" + userId_not_in: [Int] + + """Filter by the id of the user who sent a message""" + messengerId_not: Int + + """Filter by the id of the user who sent a message""" + messengerId_in: [Int] + + """Filter by the id of the user who sent a message""" + messengerId_not_in: [Int] + + """Filter by the associated media id of the activity""" + mediaId_not: Int + + """Filter by the associated media id of the activity""" + mediaId_in: [Int] + + """Filter by the associated media id of the activity""" + mediaId_not_in: [Int] + + """Filter by the type of activity""" + type_not: ActivityType + + """Filter by the type of activity""" + type_in: [ActivityType] + + """Filter by the type of activity""" + type_not_in: [ActivityType] + + """Filter by the time the activity was created""" + createdAt_greater: Int + + """Filter by the time the activity was created""" + createdAt_lesser: Int + + """The order the results will be returned in""" + sort: [ActivitySort] + ): ActivityUnion + + """Activity reply query""" + ActivityReply( + """Filter by the reply id""" + id: Int + + """Filter by the parent id""" + activityId: Int + ): ActivityReply + + """Follow query""" + Following( + """User id of the follower/followed""" + userId: Int! + + """The order the results will be returned in""" + sort: [UserSort] + ): User + + """Follow query""" + Follower( + """User id of the follower/followed""" + userId: Int! + + """The order the results will be returned in""" + sort: [UserSort] + ): User + + """Thread query""" + Thread( + """Filter by the thread id""" + id: Int + + """Filter by the user id of the thread's creator""" + userId: Int + + """Filter by the user id of the last user to comment on the thread""" + replyUserId: Int + + """Filter by if the currently authenticated user's subscribed threads""" + subscribed: Boolean + + """Filter by thread category id""" + categoryId: Int + + """Filter by thread media id category""" + mediaCategoryId: Int + + """Filter by search query""" + search: String + + """Filter by the thread id""" + id_in: [Int] + + """The order the results will be returned in""" + sort: [ThreadSort] + ): Thread + + """Comment query""" + ThreadComment( + """Filter by the comment id""" + id: Int + + """Filter by the thread id""" + threadId: Int + + """Filter by the user id of the comment's creator""" + userId: Int + + """The order the results will be returned in""" + sort: [ThreadCommentSort] + ): [ThreadComment] + + """Recommendation query""" + Recommendation( + """Filter by recommendation id""" + id: Int + + """Filter by media id""" + mediaId: Int + + """Filter by media recommendation id""" + mediaRecommendationId: Int + + """Filter by user who created the recommendation""" + userId: Int + + """Filter by total rating of the recommendation""" + rating: Int + + """Filter by the media on the authenticated user's lists""" + onList: Boolean + + """Filter by total rating of the recommendation""" + rating_greater: Int + + """Filter by total rating of the recommendation""" + rating_lesser: Int + + """The order the results will be returned in""" + sort: [RecommendationSort] + ): Recommendation + + """Like query""" + Like( + """The id of the likeable type""" + likeableId: Int + + """The type of model the id applies to""" + type: LikeableType + ): User + + """Provide AniList markdown to be converted to html (Requires auth)""" + Markdown( + """The markdown to be parsed to html""" + markdown: String! + ): ParsedMarkdown + AniChartUser: AniChartUser + + """Site statistics query""" + SiteStatistics: SiteStatistics + + """ExternalLinkSource collection query""" + ExternalLinkSourceCollection( + """Filter by the link id""" + id: Int + type: ExternalLinkType + mediaType: ExternalLinkMediaType + ): [MediaExternalLink] +} + +"""Page of data""" +type Page { + """The pagination information""" + pageInfo: PageInfo + users( + """Filter by the user id""" + id: Int + + """Filter by the name of the user""" + name: String + + """Filter to moderators only if true""" + isModerator: Boolean + + """Filter by search query""" + search: String + + """The order the results will be returned in""" + sort: [UserSort] + ): [User] + media( + """Filter by the media id""" + id: Int + + """Filter by the media's MyAnimeList id""" + idMal: Int + + """Filter by the start date of the media""" + startDate: FuzzyDateInt + + """Filter by the end date of the media""" + endDate: FuzzyDateInt + + """Filter by the season the media was released in""" + season: MediaSeason + + """ + The year of the season (Winter 2017 would also include December 2016 releases). Requires season argument + """ + seasonYear: Int + + """Filter by the media's type""" + type: MediaType + + """Filter by the media's format""" + format: MediaFormat + + """Filter by the media's current release status""" + status: MediaStatus + + """Filter by amount of episodes the media has""" + episodes: Int + + """Filter by the media's episode length""" + duration: Int + + """Filter by the media's chapter count""" + chapters: Int + + """Filter by the media's volume count""" + volumes: Int + + """Filter by if the media's intended for 18+ adult audiences""" + isAdult: Boolean + + """Filter by the media's genres""" + genre: String + + """Filter by the media's tags""" + tag: String + + """ + Only apply the tags filter argument to tags above this rank. Default: 18 + """ + minimumTagRank: Int + + """Filter by the media's tags with in a tag category""" + tagCategory: String + + """Filter by the media on the authenticated user's lists""" + onList: Boolean + + """Filter media by sites name with a online streaming or reading license""" + licensedBy: String + + """Filter media by sites id with a online streaming or reading license""" + licensedById: Int + + """Filter by the media's average score""" + averageScore: Int + + """Filter by the number of users with this media on their list""" + popularity: Int + + """Filter by the source type of the media""" + source: MediaSource + + """Filter by the media's country of origin""" + countryOfOrigin: CountryCode + + """If the media is officially licensed or a self-published doujin release""" + isLicensed: Boolean + + """Filter by search query""" + search: String + + """Filter by the media id""" + id_not: Int + + """Filter by the media id""" + id_in: [Int] + + """Filter by the media id""" + id_not_in: [Int] + + """Filter by the media's MyAnimeList id""" + idMal_not: Int + + """Filter by the media's MyAnimeList id""" + idMal_in: [Int] + + """Filter by the media's MyAnimeList id""" + idMal_not_in: [Int] + + """Filter by the start date of the media""" + startDate_greater: FuzzyDateInt + + """Filter by the start date of the media""" + startDate_lesser: FuzzyDateInt + + """Filter by the start date of the media""" + startDate_like: String + + """Filter by the end date of the media""" + endDate_greater: FuzzyDateInt + + """Filter by the end date of the media""" + endDate_lesser: FuzzyDateInt + + """Filter by the end date of the media""" + endDate_like: String + + """Filter by the media's format""" + format_in: [MediaFormat] + + """Filter by the media's format""" + format_not: MediaFormat + + """Filter by the media's format""" + format_not_in: [MediaFormat] + + """Filter by the media's current release status""" + status_in: [MediaStatus] + + """Filter by the media's current release status""" + status_not: MediaStatus + + """Filter by the media's current release status""" + status_not_in: [MediaStatus] + + """Filter by amount of episodes the media has""" + episodes_greater: Int + + """Filter by amount of episodes the media has""" + episodes_lesser: Int + + """Filter by the media's episode length""" + duration_greater: Int + + """Filter by the media's episode length""" + duration_lesser: Int + + """Filter by the media's chapter count""" + chapters_greater: Int + + """Filter by the media's chapter count""" + chapters_lesser: Int + + """Filter by the media's volume count""" + volumes_greater: Int + + """Filter by the media's volume count""" + volumes_lesser: Int + + """Filter by the media's genres""" + genre_in: [String] + + """Filter by the media's genres""" + genre_not_in: [String] + + """Filter by the media's tags""" + tag_in: [String] + + """Filter by the media's tags""" + tag_not_in: [String] + + """Filter by the media's tags with in a tag category""" + tagCategory_in: [String] + + """Filter by the media's tags with in a tag category""" + tagCategory_not_in: [String] + + """Filter media by sites name with a online streaming or reading license""" + licensedBy_in: [String] + + """Filter media by sites id with a online streaming or reading license""" + licensedById_in: [Int] + + """Filter by the media's average score""" + averageScore_not: Int + + """Filter by the media's average score""" + averageScore_greater: Int + + """Filter by the media's average score""" + averageScore_lesser: Int + + """Filter by the number of users with this media on their list""" + popularity_not: Int + + """Filter by the number of users with this media on their list""" + popularity_greater: Int + + """Filter by the number of users with this media on their list""" + popularity_lesser: Int + + """Filter by the source type of the media""" + source_in: [MediaSource] + + """The order the results will be returned in""" + sort: [MediaSort] + ): [Media] + characters( + """Filter by character id""" + id: Int + + """Filter by character by if its their birthday today""" + isBirthday: Boolean + + """Filter by search query""" + search: String + + """Filter by character id""" + id_not: Int + + """Filter by character id""" + id_in: [Int] + + """Filter by character id""" + id_not_in: [Int] + + """The order the results will be returned in""" + sort: [CharacterSort] + ): [Character] + staff( + """Filter by the staff id""" + id: Int + + """Filter by staff by if its their birthday today""" + isBirthday: Boolean + + """Filter by search query""" + search: String + + """Filter by the staff id""" + id_not: Int + + """Filter by the staff id""" + id_in: [Int] + + """Filter by the staff id""" + id_not_in: [Int] + + """The order the results will be returned in""" + sort: [StaffSort] + ): [Staff] + studios( + """Filter by the studio id""" + id: Int + + """Filter by search query""" + search: String + + """Filter by the studio id""" + id_not: Int + + """Filter by the studio id""" + id_in: [Int] + + """Filter by the studio id""" + id_not_in: [Int] + + """The order the results will be returned in""" + sort: [StudioSort] + ): [Studio] + mediaList( + """Filter by a list entry's id""" + id: Int + + """Filter by a user's id""" + userId: Int + + """Filter by a user's name""" + userName: String + + """Filter by the list entries media type""" + type: MediaType + + """Filter by the watching/reading status""" + status: MediaListStatus + + """Filter by the media id of the list entry""" + mediaId: Int + + """ + Filter list entries to users who are being followed by the authenticated user + """ + isFollowing: Boolean + + """Filter by note words and #tags""" + notes: String + + """Filter by the date the user started the media""" + startedAt: FuzzyDateInt + + """Filter by the date the user completed the media""" + completedAt: FuzzyDateInt + + """ + Limit to only entries also on the auth user's list. Requires user id or name arguments. + """ + compareWithAuthList: Boolean + + """Filter by a user's id""" + userId_in: [Int] + + """Filter by the watching/reading status""" + status_in: [MediaListStatus] + + """Filter by the watching/reading status""" + status_not_in: [MediaListStatus] + + """Filter by the watching/reading status""" + status_not: MediaListStatus + + """Filter by the media id of the list entry""" + mediaId_in: [Int] + + """Filter by the media id of the list entry""" + mediaId_not_in: [Int] + + """Filter by note words and #tags""" + notes_like: String + + """Filter by the date the user started the media""" + startedAt_greater: FuzzyDateInt + + """Filter by the date the user started the media""" + startedAt_lesser: FuzzyDateInt + + """Filter by the date the user started the media""" + startedAt_like: String + + """Filter by the date the user completed the media""" + completedAt_greater: FuzzyDateInt + + """Filter by the date the user completed the media""" + completedAt_lesser: FuzzyDateInt + + """Filter by the date the user completed the media""" + completedAt_like: String + + """The order the results will be returned in""" + sort: [MediaListSort] + ): [MediaList] + airingSchedules( + """Filter by the id of the airing schedule item""" + id: Int + + """Filter by the id of associated media""" + mediaId: Int + + """Filter by the airing episode number""" + episode: Int + + """Filter by the time of airing""" + airingAt: Int + + """Filter to episodes that haven't yet aired""" + notYetAired: Boolean + + """Filter by the id of the airing schedule item""" + id_not: Int + + """Filter by the id of the airing schedule item""" + id_in: [Int] + + """Filter by the id of the airing schedule item""" + id_not_in: [Int] + + """Filter by the id of associated media""" + mediaId_not: Int + + """Filter by the id of associated media""" + mediaId_in: [Int] + + """Filter by the id of associated media""" + mediaId_not_in: [Int] + + """Filter by the airing episode number""" + episode_not: Int + + """Filter by the airing episode number""" + episode_in: [Int] + + """Filter by the airing episode number""" + episode_not_in: [Int] + + """Filter by the airing episode number""" + episode_greater: Int + + """Filter by the airing episode number""" + episode_lesser: Int + + """Filter by the time of airing""" + airingAt_greater: Int + + """Filter by the time of airing""" + airingAt_lesser: Int + + """The order the results will be returned in""" + sort: [AiringSort] + ): [AiringSchedule] + mediaTrends( + """Filter by the media id""" + mediaId: Int + + """Filter by date""" + date: Int + + """Filter by trending amount""" + trending: Int + + """Filter by score""" + averageScore: Int + + """Filter by popularity""" + popularity: Int + + """Filter by episode number""" + episode: Int + + """Filter to stats recorded while the media was releasing""" + releasing: Boolean + + """Filter by the media id""" + mediaId_not: Int + + """Filter by the media id""" + mediaId_in: [Int] + + """Filter by the media id""" + mediaId_not_in: [Int] + + """Filter by date""" + date_greater: Int + + """Filter by date""" + date_lesser: Int + + """Filter by trending amount""" + trending_greater: Int + + """Filter by trending amount""" + trending_lesser: Int + + """Filter by trending amount""" + trending_not: Int + + """Filter by score""" + averageScore_greater: Int + + """Filter by score""" + averageScore_lesser: Int + + """Filter by score""" + averageScore_not: Int + + """Filter by popularity""" + popularity_greater: Int + + """Filter by popularity""" + popularity_lesser: Int + + """Filter by popularity""" + popularity_not: Int + + """Filter by episode number""" + episode_greater: Int + + """Filter by episode number""" + episode_lesser: Int + + """Filter by episode number""" + episode_not: Int + + """The order the results will be returned in""" + sort: [MediaTrendSort] + ): [MediaTrend] + notifications( + """Filter by the type of notifications""" + type: NotificationType + + """Reset the unread notification count to 0 on load""" + resetNotificationCount: Boolean + + """Filter by the type of notifications""" + type_in: [NotificationType] + ): [NotificationUnion] + followers( + """User id of the follower/followed""" + userId: Int! + + """The order the results will be returned in""" + sort: [UserSort] + ): [User] + following( + """User id of the follower/followed""" + userId: Int! + + """The order the results will be returned in""" + sort: [UserSort] + ): [User] + activities( + """Filter by the activity id""" + id: Int + + """Filter by the owner user id""" + userId: Int + + """Filter by the id of the user who sent a message""" + messengerId: Int + + """Filter by the associated media id of the activity""" + mediaId: Int + + """Filter by the type of activity""" + type: ActivityType + + """ + Filter activity to users who are being followed by the authenticated user + """ + isFollowing: Boolean + + """Filter activity to only activity with replies""" + hasReplies: Boolean + + """Filter activity to only activity with replies or is of type text""" + hasRepliesOrTypeText: Boolean + + """Filter by the time the activity was created""" + createdAt: Int + + """Filter by the activity id""" + id_not: Int + + """Filter by the activity id""" + id_in: [Int] + + """Filter by the activity id""" + id_not_in: [Int] + + """Filter by the owner user id""" + userId_not: Int + + """Filter by the owner user id""" + userId_in: [Int] + + """Filter by the owner user id""" + userId_not_in: [Int] + + """Filter by the id of the user who sent a message""" + messengerId_not: Int + + """Filter by the id of the user who sent a message""" + messengerId_in: [Int] + + """Filter by the id of the user who sent a message""" + messengerId_not_in: [Int] + + """Filter by the associated media id of the activity""" + mediaId_not: Int + + """Filter by the associated media id of the activity""" + mediaId_in: [Int] + + """Filter by the associated media id of the activity""" + mediaId_not_in: [Int] + + """Filter by the type of activity""" + type_not: ActivityType + + """Filter by the type of activity""" + type_in: [ActivityType] + + """Filter by the type of activity""" + type_not_in: [ActivityType] + + """Filter by the time the activity was created""" + createdAt_greater: Int + + """Filter by the time the activity was created""" + createdAt_lesser: Int + + """The order the results will be returned in""" + sort: [ActivitySort] + ): [ActivityUnion] + activityReplies( + """Filter by the reply id""" + id: Int + + """Filter by the parent id""" + activityId: Int + ): [ActivityReply] + threads( + """Filter by the thread id""" + id: Int + + """Filter by the user id of the thread's creator""" + userId: Int + + """Filter by the user id of the last user to comment on the thread""" + replyUserId: Int + + """Filter by if the currently authenticated user's subscribed threads""" + subscribed: Boolean + + """Filter by thread category id""" + categoryId: Int + + """Filter by thread media id category""" + mediaCategoryId: Int + + """Filter by search query""" + search: String + + """Filter by the thread id""" + id_in: [Int] + + """The order the results will be returned in""" + sort: [ThreadSort] + ): [Thread] + threadComments( + """Filter by the comment id""" + id: Int + + """Filter by the thread id""" + threadId: Int + + """Filter by the user id of the comment's creator""" + userId: Int + + """The order the results will be returned in""" + sort: [ThreadCommentSort] + ): [ThreadComment] + reviews( + """Filter by Review id""" + id: Int + + """Filter by media id""" + mediaId: Int + + """Filter by user id""" + userId: Int + + """Filter by media type""" + mediaType: MediaType + + """The order the results will be returned in""" + sort: [ReviewSort] + ): [Review] + recommendations( + """Filter by recommendation id""" + id: Int + + """Filter by media id""" + mediaId: Int + + """Filter by media recommendation id""" + mediaRecommendationId: Int + + """Filter by user who created the recommendation""" + userId: Int + + """Filter by total rating of the recommendation""" + rating: Int + + """Filter by the media on the authenticated user's lists""" + onList: Boolean + + """Filter by total rating of the recommendation""" + rating_greater: Int + + """Filter by total rating of the recommendation""" + rating_lesser: Int + + """The order the results will be returned in""" + sort: [RecommendationSort] + ): [Recommendation] + likes( + """The id of the likeable type""" + likeableId: Int + + """The type of model the id applies to""" + type: LikeableType + ): [User] +} + +type PageInfo { + """ + The total number of items. Note: This value is not guaranteed to be accurate, do not rely on this for logic + """ + total: Int + + """The count on a page""" + perPage: Int + + """The current page""" + currentPage: Int + + """The last page""" + lastPage: Int + + """If there is another page""" + hasNextPage: Boolean +} + +"""User sort enums""" +enum UserSort { + ID + ID_DESC + USERNAME + USERNAME_DESC + WATCHED_TIME + WATCHED_TIME_DESC + CHAPTERS_READ + CHAPTERS_READ_DESC + SEARCH_MATCH +} + +"""A user""" +type User { + """The id of the user""" + id: Int! + + """The name of the user""" + name: String! + + """The bio written by user (Markdown)""" + about( + """Return the string in pre-parsed html instead of markdown""" + asHtml: Boolean + ): String + + """The user's avatar images""" + avatar: UserAvatar + + """The user's banner images""" + bannerImage: String + + """If the authenticated user if following this user""" + isFollowing: Boolean + + """If this user if following the authenticated user""" + isFollower: Boolean + + """If the user is blocked by the authenticated user""" + isBlocked: Boolean + bans: Json + + """The user's general options""" + options: UserOptions + + """The user's media list options""" + mediaListOptions: MediaListOptions + + """The users favourites""" + favourites( + """Deprecated. Use page arguments on each favourite field instead.""" + page: Int + ): Favourites + + """The users anime & manga list statistics""" + statistics: UserStatisticTypes + + """The number of unread notifications the user has""" + unreadNotificationCount: Int + + """The url for the user page on the AniList website""" + siteUrl: String + + """The donation tier of the user""" + donatorTier: Int + + """Custom donation badge text""" + donatorBadge: String + + """The user's moderator roles if they are a site moderator""" + moderatorRoles: [ModRole] + + """ + When the user's account was created. (Does not exist for accounts created before 2020) + """ + createdAt: Int + + """When the user's data was last updated""" + updatedAt: Int + + """The user's statistics""" + stats: UserStats @deprecated(reason: "Deprecated. Replaced with statistics field.") + + """If the user is a moderator or data moderator""" + moderatorStatus: String @deprecated(reason: "Deprecated. Replaced with moderatorRoles field.") + + """The user's previously used names.""" + previousNames: [UserPreviousName] +} + +"""A user's avatars""" +type UserAvatar { + """The avatar of user at its largest size""" + large: String + + """The avatar of user at medium size""" + medium: String +} + +"""""" +scalar Json + +"""A user's general options""" +type UserOptions { + """The language the user wants to see media titles in""" + titleLanguage: UserTitleLanguage + + """Whether the user has enabled viewing of 18+ content""" + displayAdultContent: Boolean + + """ + Whether the user receives notifications when a show they are watching aires + """ + airingNotifications: Boolean + + """Profile highlight color (blue, purple, pink, orange, red, green, gray)""" + profileColor: String + + """Notification options""" + notificationOptions: [NotificationOption] + + """The user's timezone offset (Auth user only)""" + timezone: String + + """ + Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always. + """ + activityMergeTime: Int + + """The language the user wants to see staff and character names in""" + staffNameLanguage: UserStaffNameLanguage + + """Whether the user only allow messages from users they follow""" + restrictMessagesToFollowing: Boolean + + """ + The list activity types the user has disabled from being created from list updates + """ + disabledListActivity: [ListActivityOption] +} + +"""The language the user wants to see media titles in""" +enum UserTitleLanguage { + """The romanization of the native language title""" + ROMAJI + + """The official english title""" + ENGLISH + + """Official title in it's native language""" + NATIVE + + """ + The romanization of the native language title, stylised by media creator + """ + ROMAJI_STYLISED + + """The official english title, stylised by media creator""" + ENGLISH_STYLISED + + """Official title in it's native language, stylised by media creator""" + NATIVE_STYLISED +} + +"""Notification option""" +type NotificationOption { + """The type of notification""" + type: NotificationType + + """Whether this type of notification is enabled""" + enabled: Boolean +} + +"""Notification type enum""" +enum NotificationType { + """A user has sent you message""" + ACTIVITY_MESSAGE + + """A user has replied to your activity""" + ACTIVITY_REPLY + + """A user has followed you""" + FOLLOWING + + """A user has mentioned you in their activity""" + ACTIVITY_MENTION + + """A user has mentioned you in a forum comment""" + THREAD_COMMENT_MENTION + + """A user has commented in one of your subscribed forum threads""" + THREAD_SUBSCRIBED + + """A user has replied to your forum comment""" + THREAD_COMMENT_REPLY + + """An anime you are currently watching has aired""" + AIRING + + """A user has liked your activity""" + ACTIVITY_LIKE + + """A user has liked your activity reply""" + ACTIVITY_REPLY_LIKE + + """A user has liked your forum thread""" + THREAD_LIKE + + """A user has liked your forum comment""" + THREAD_COMMENT_LIKE + + """A user has replied to activity you have also replied to""" + ACTIVITY_REPLY_SUBSCRIBED + + """ + A new anime or manga has been added to the site where its related media is on the user's list + """ + RELATED_MEDIA_ADDITION + + """ + An anime or manga has had a data change that affects how a user may track it in their lists + """ + MEDIA_DATA_CHANGE + + """ + Anime or manga entries on the user's list have been merged into a single entry + """ + MEDIA_MERGE + + """An anime or manga on the user's list has been deleted from the site""" + MEDIA_DELETION +} + +"""The language the user wants to see staff and character names in""" +enum UserStaffNameLanguage { + """ + The romanization of the staff or character's native name, with western name ordering + """ + ROMAJI_WESTERN + + """The romanization of the staff or character's native name""" + ROMAJI + + """The staff or character's name in their native language""" + NATIVE +} + +type ListActivityOption { + disabled: Boolean + type: MediaListStatus +} + +"""Media list watching/reading status enum.""" +enum MediaListStatus { + """Currently watching/reading""" + CURRENT + + """Planning to watch/read""" + PLANNING + + """Finished watching/reading""" + COMPLETED + + """Stopped watching/reading before completing""" + DROPPED + + """Paused watching/reading""" + PAUSED + + """Re-watching/reading""" + REPEATING +} + +"""A user's list options""" +type MediaListOptions { + """The score format the user is using for media lists""" + scoreFormat: ScoreFormat + + """The default order list rows should be displayed in""" + rowOrder: String + useLegacyLists: Boolean @deprecated(reason: "No longer used") + + """The user's anime list options""" + animeList: MediaListTypeOptions + + """The user's manga list options""" + mangaList: MediaListTypeOptions + + """The list theme options for both lists""" + sharedTheme: Json @deprecated(reason: "No longer used") + + """ + If the shared theme should be used instead of the individual list themes + """ + sharedThemeEnabled: Boolean @deprecated(reason: "No longer used") +} + +"""Media list scoring type""" +enum ScoreFormat { + """An integer from 0-100""" + POINT_100 + + """A float from 0-10 with 1 decimal place""" + POINT_10_DECIMAL + + """An integer from 0-10""" + POINT_10 + + """An integer from 0-5. Should be represented in Stars""" + POINT_5 + + """ + An integer from 0-3. Should be represented in Smileys. 0 => No Score, 1 => :(, 2 => :|, 3 => :) + """ + POINT_3 +} + +"""A user's list options for anime or manga lists""" +type MediaListTypeOptions { + """The order each list should be displayed in""" + sectionOrder: [String] + + """If the completed sections of the list should be separated by format""" + splitCompletedSectionByFormat: Boolean + + """The list theme options""" + theme: Json @deprecated(reason: "This field has not yet been fully implemented and may change without warning") + + """The names of the user's custom lists""" + customLists: [String] + + """The names of the user's advanced scoring sections""" + advancedScoring: [String] + + """If advanced scoring is enabled""" + advancedScoringEnabled: Boolean +} + +"""User's favourite anime, manga, characters, staff & studios""" +type Favourites { + """Favourite anime""" + anime( + """The page number""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): MediaConnection + + """Favourite manga""" + manga( + """The page number""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): MediaConnection + + """Favourite characters""" + characters( + """The page number""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): CharacterConnection + + """Favourite staff""" + staff( + """The page number""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): StaffConnection + + """Favourite studios""" + studios( + """The page number""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): StudioConnection +} + +type MediaConnection { + edges: [MediaEdge] + nodes: [Media] + + """The pagination information""" + pageInfo: PageInfo +} + +"""Media connection edge""" +type MediaEdge { + node: Media + + """The id of the connection""" + id: Int + + """The type of relation to the parent model""" + relationType( + """Provide 2 to use new version 2 of relation enum""" + version: Int + ): MediaRelation + + """ + If the studio is the main animation studio of the media (For Studio->MediaConnection field only) + """ + isMainStudio: Boolean! + + """The characters in the media voiced by the parent actor""" + characters: [Character] + + """The characters role in the media""" + characterRole: CharacterRole + + """Media specific character name""" + characterName: String + + """Notes regarding the VA's role for the character""" + roleNotes: String + + """ + Used for grouping roles where multiple dubs exist for the same language. Either dubbing company name or language variant. + """ + dubGroup: String + + """The role of the staff member in the production of the media""" + staffRole: String + + """The voice actors of the character""" + voiceActors(language: StaffLanguage, sort: [StaffSort]): [Staff] + + """The voice actors of the character with role date""" + voiceActorRoles(language: StaffLanguage, sort: [StaffSort]): [StaffRoleType] + + """The order the media should be displayed from the users favourites""" + favouriteOrder: Int +} + +"""Anime or Manga""" +type Media { + """The id of the media""" + id: Int! + + """The mal id of the media""" + idMal: Int + + """The official titles of the media in various languages""" + title: MediaTitle + + """The type of the media; anime or manga""" + type: MediaType + + """The format the media was released in""" + format: MediaFormat + + """The current releasing status of the media""" + status( + """Provide 2 to use new version 2 of sources enum""" + version: Int + ): MediaStatus + + """Short description of the media's story and characters""" + description( + """Return the string in pre-parsed html instead of markdown""" + asHtml: Boolean + ): String + + """The first official release date of the media""" + startDate: FuzzyDate + + """The last official release date of the media""" + endDate: FuzzyDate + + """The season the media was initially released in""" + season: MediaSeason + + """The season year the media was initially released in""" + seasonYear: Int + + """The year & season the media was initially released in""" + seasonInt: Int @deprecated(reason: "") + + """The amount of episodes the anime has when complete""" + episodes: Int + + """The general length of each anime episode in minutes""" + duration: Int + + """The amount of chapters the manga has when complete""" + chapters: Int + + """The amount of volumes the manga has when complete""" + volumes: Int + + """Where the media was created. (ISO 3166-1 alpha-2)""" + countryOfOrigin: CountryCode + + """If the media is officially licensed or a self-published doujin release""" + isLicensed: Boolean + + """Source type the media was adapted from.""" + source( + """Provide 2 or 3 to use new version 2 or 3 of sources enum""" + version: Int + ): MediaSource + + """Official Twitter hashtags for the media""" + hashtag: String + + """Media trailer or advertisement""" + trailer: MediaTrailer + + """When the media's data was last updated""" + updatedAt: Int + + """The cover images of the media""" + coverImage: MediaCoverImage + + """The banner image of the media""" + bannerImage: String + + """The genres of the media""" + genres: [String] + + """Alternative titles of the media""" + synonyms: [String] + + """A weighted average score of all the user's scores of the media""" + averageScore: Int + + """Mean score of all the user's scores of the media""" + meanScore: Int + + """The number of users with the media on their list""" + popularity: Int + + """ + Locked media may not be added to lists our favorited. This may be due to the entry pending for deletion or other reasons. + """ + isLocked: Boolean + + """The amount of related activity in the past hour""" + trending: Int + + """The amount of user's who have favourited the media""" + favourites: Int + + """List of tags that describes elements and themes of the media""" + tags: [MediaTag] + + """Other media in the same or connecting franchise""" + relations: MediaConnection + + """The characters in the media""" + characters( + sort: [CharacterSort] + role: CharacterRole + + """The page""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): CharacterConnection + + """The staff who produced the media""" + staff( + sort: [StaffSort] + + """The page""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): StaffConnection + + """The companies who produced the media""" + studios(sort: [StudioSort], isMain: Boolean): StudioConnection + + """If the media is marked as favourite by the current authenticated user""" + isFavourite: Boolean! + + """If the media is blocked from being added to favourites""" + isFavouriteBlocked: Boolean! + + """If the media is intended only for 18+ adult audiences""" + isAdult: Boolean + + """The media's next episode airing schedule""" + nextAiringEpisode: AiringSchedule + + """The media's entire airing schedule""" + airingSchedule( + """Filter to episodes that have not yet aired""" + notYetAired: Boolean + + """The page""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): AiringScheduleConnection + + """The media's daily trend stats""" + trends( + sort: [MediaTrendSort] + + """Filter to stats recorded while the media was releasing""" + releasing: Boolean + + """The page""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): MediaTrendConnection + + """External links to another site related to the media""" + externalLinks: [MediaExternalLink] + + """Data and links to legal streaming episodes on external sites""" + streamingEpisodes: [MediaStreamingEpisode] + + """ + The ranking of the media in a particular time span and format compared to other media + """ + rankings: [MediaRank] + + """The authenticated user's media list entry for the media""" + mediaListEntry: MediaList + + """User reviews of the media""" + reviews( + limit: Int + sort: [ReviewSort] + + """The page""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): ReviewConnection + + """User recommendations for similar media""" + recommendations( + sort: [RecommendationSort] + + """The page""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): RecommendationConnection + stats: MediaStats + + """The url for the media page on the AniList website""" + siteUrl: String + + """ + If the media should have forum thread automatically created for it on airing episode release + """ + autoCreateForumThread: Boolean + + """If the media is blocked from being recommended to/from""" + isRecommendationBlocked: Boolean + + """If the media is blocked from being reviewed""" + isReviewBlocked: Boolean + + """Notes for site moderators""" + modNotes: String +} + +"""The official titles of the media in various languages""" +type MediaTitle { + """The romanization of the native language title""" + romaji(stylised: Boolean): String + + """The official english title""" + english(stylised: Boolean): String + + """Official title in it's native language""" + native(stylised: Boolean): String + + """ + The currently authenticated users preferred title language. Default romaji for non-authenticated + """ + userPreferred: String +} + +"""Media type enum, anime or manga.""" +enum MediaType { + """Japanese Anime""" + ANIME + + """Asian comic""" + MANGA +} + +"""The format the media was released in""" +enum MediaFormat { + """Anime broadcast on television""" + TV + + """Anime which are under 15 minutes in length and broadcast on television""" + TV_SHORT + + """Anime movies with a theatrical release""" + MOVIE + + """ + Special episodes that have been included in DVD/Blu-ray releases, picture dramas, pilots, etc + """ + SPECIAL + + """ + (Original Video Animation) Anime that have been released directly on DVD/Blu-ray without originally going through a theatrical release or television broadcast + """ + OVA + + """ + (Original Net Animation) Anime that have been originally released online or are only available through streaming services. + """ + ONA + + """Short anime released as a music video""" + MUSIC + + """Professionally published manga with more than one chapter""" + MANGA + + """Written books released as a series of light novels""" + NOVEL + + """Manga with just one chapter""" + ONE_SHOT +} + +"""The current releasing status of the media""" +enum MediaStatus { + """Has completed and is no longer being released""" + FINISHED + + """Currently releasing""" + RELEASING + + """To be released at a later date""" + NOT_YET_RELEASED + + """Ended before the work could be finished""" + CANCELLED + + """ + Version 2 only. Is currently paused from releasing and will resume at a later date + """ + HIATUS +} + +"""Date object that allows for incomplete date values (fuzzy)""" +type FuzzyDate { + """Numeric Year (2017)""" + year: Int + + """Numeric Month (3)""" + month: Int + + """Numeric Day (24)""" + day: Int +} + +enum MediaSeason { + """Months December to February""" + WINTER + + """Months March to May""" + SPRING + + """Months June to August""" + SUMMER + + """Months September to November""" + FALL +} + +"""ISO 3166-1 alpha-2 country code""" +scalar CountryCode + +"""Source type the media was adapted from""" +enum MediaSource { + """An original production not based of another work""" + ORIGINAL + + """Asian comic book""" + MANGA + + """Written work published in volumes""" + LIGHT_NOVEL + + """Video game driven primary by text and narrative""" + VISUAL_NOVEL + + """Video game""" + VIDEO_GAME + + """Other""" + OTHER + + """Version 2+ only. Written works not published in volumes""" + NOVEL + + """Version 2+ only. Self-published works""" + DOUJINSHI + + """Version 2+ only. Japanese Anime""" + ANIME + + """Version 3 only. Written works published online""" + WEB_NOVEL + + """Version 3 only. Live action media such as movies or TV show""" + LIVE_ACTION + + """Version 3 only. Games excluding video games""" + GAME + + """Version 3 only. Comics excluding manga""" + COMIC + + """Version 3 only. Multimedia project""" + MULTIMEDIA_PROJECT + + """Version 3 only. Picture book""" + PICTURE_BOOK +} + +"""Media trailer or advertisement""" +type MediaTrailer { + """The trailer video id""" + id: String + + """ + The site the video is hosted by (Currently either youtube or dailymotion) + """ + site: String + + """The url for the thumbnail image of the video""" + thumbnail: String +} + +type MediaCoverImage { + """ + The cover image url of the media at its largest size. If this size isn't available, large will be provided instead. + """ + extraLarge: String + + """The cover image url of the media at a large size""" + large: String + + """The cover image url of the media at medium size""" + medium: String + + """Average #hex color of cover image""" + color: String +} + +"""A tag that describes a theme or element of the media""" +type MediaTag { + """The id of the tag""" + id: Int! + + """The name of the tag""" + name: String! + + """A general description of the tag""" + description: String + + """The categories of tags this tag belongs to""" + category: String + + """The relevance ranking of the tag out of the 100 for this media""" + rank: Int + + """If the tag could be a spoiler for any media""" + isGeneralSpoiler: Boolean + + """If the tag is a spoiler for this media""" + isMediaSpoiler: Boolean + + """If the tag is only for adult 18+ media""" + isAdult: Boolean + + """The user who submitted the tag""" + userId: Int +} + +"""Character sort enums""" +enum CharacterSort { + ID + ID_DESC + ROLE + ROLE_DESC + SEARCH_MATCH + FAVOURITES + FAVOURITES_DESC + + """Order manually decided by moderators""" + RELEVANCE +} + +"""The role the character plays in the media""" +enum CharacterRole { + """A primary character role in the media""" + MAIN + + """A supporting character role in the media""" + SUPPORTING + + """A background character in the media""" + BACKGROUND +} + +type CharacterConnection { + edges: [CharacterEdge] + nodes: [Character] + + """The pagination information""" + pageInfo: PageInfo +} + +"""Character connection edge""" +type CharacterEdge { + node: Character + + """The id of the connection""" + id: Int + + """The characters role in the media""" + role: CharacterRole + + """Media specific character name""" + name: String + + """The voice actors of the character""" + voiceActors(language: StaffLanguage, sort: [StaffSort]): [Staff] + + """The voice actors of the character with role date""" + voiceActorRoles(language: StaffLanguage, sort: [StaffSort]): [StaffRoleType] + + """The media the character is in""" + media: [Media] + + """The order the character should be displayed from the users favourites""" + favouriteOrder: Int +} + +"""A character that features in an anime or manga""" +type Character { + """The id of the character""" + id: Int! + + """The names of the character""" + name: CharacterName + + """Character images""" + image: CharacterImage + + """A general description of the character""" + description( + """Return the string in pre-parsed html instead of markdown""" + asHtml: Boolean + ): String + + """ + The character's gender. Usually Male, Female, or Non-binary but can be any string. + """ + gender: String + + """The character's birth date""" + dateOfBirth: FuzzyDate + + """ + The character's age. Note this is a string, not an int, it may contain further text and additional ages. + """ + age: String + + """The characters blood type""" + bloodType: String + + """ + If the character is marked as favourite by the currently authenticated user + """ + isFavourite: Boolean! + + """If the character is blocked from being added to favourites""" + isFavouriteBlocked: Boolean! + + """The url for the character page on the AniList website""" + siteUrl: String + + """Media that includes the character""" + media( + sort: [MediaSort] + type: MediaType + onList: Boolean + + """The page""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): MediaConnection + updatedAt: Int @deprecated(reason: "No data available") + + """The amount of user's who have favourited the character""" + favourites: Int + + """Notes for site moderators""" + modNotes: String +} + +"""The names of the character""" +type CharacterName { + """The character's given name""" + first: String + + """The character's middle name""" + middle: String + + """The character's surname""" + last: String + + """The character's first and last name""" + full: String + + """The character's full name in their native language""" + native: String + + """Other names the character might be referred to as""" + alternative: [String] + + """Other names the character might be referred to as but are spoilers""" + alternativeSpoiler: [String] + + """ + The currently authenticated users preferred name language. Default romaji for non-authenticated + """ + userPreferred: String +} + +type CharacterImage { + """The character's image of media at its largest size""" + large: String + + """The character's image of media at medium size""" + medium: String +} + +"""Media sort enums""" +enum MediaSort { + ID + ID_DESC + TITLE_ROMAJI + TITLE_ROMAJI_DESC + TITLE_ENGLISH + TITLE_ENGLISH_DESC + TITLE_NATIVE + TITLE_NATIVE_DESC + TYPE + TYPE_DESC + FORMAT + FORMAT_DESC + START_DATE + START_DATE_DESC + END_DATE + END_DATE_DESC + SCORE + SCORE_DESC + POPULARITY + POPULARITY_DESC + TRENDING + TRENDING_DESC + EPISODES + EPISODES_DESC + DURATION + DURATION_DESC + STATUS + STATUS_DESC + CHAPTERS + CHAPTERS_DESC + VOLUMES + VOLUMES_DESC + UPDATED_AT + UPDATED_AT_DESC + SEARCH_MATCH + FAVOURITES + FAVOURITES_DESC +} + +"""The primary language of the voice actor""" +enum StaffLanguage { + """Japanese""" + JAPANESE + + """English""" + ENGLISH + + """Korean""" + KOREAN + + """Italian""" + ITALIAN + + """Spanish""" + SPANISH + + """Portuguese""" + PORTUGUESE + + """French""" + FRENCH + + """German""" + GERMAN + + """Hebrew""" + HEBREW + + """Hungarian""" + HUNGARIAN +} + +"""Staff sort enums""" +enum StaffSort { + ID + ID_DESC + ROLE + ROLE_DESC + LANGUAGE + LANGUAGE_DESC + SEARCH_MATCH + FAVOURITES + FAVOURITES_DESC + + """Order manually decided by moderators""" + RELEVANCE +} + +"""Voice actors or production staff""" +type Staff { + """The id of the staff member""" + id: Int! + + """The names of the staff member""" + name: StaffName + + """The primary language the staff member dub's in""" + language: StaffLanguage @deprecated(reason: "Replaced with languageV2") + + """ + The primary language of the staff member. Current values: Japanese, English, Korean, Italian, Spanish, Portuguese, French, German, Hebrew, Hungarian, Chinese, Arabic, Filipino, Catalan, Finnish, Turkish, Dutch, Swedish, Thai, Tagalog, Malaysian, Indonesian, Vietnamese, Nepali, Hindi, Urdu + """ + languageV2: String + + """The staff images""" + image: StaffImage + + """A general description of the staff member""" + description( + """Return the string in pre-parsed html instead of markdown""" + asHtml: Boolean + ): String + + """The person's primary occupations""" + primaryOccupations: [String] + + """ + The staff's gender. Usually Male, Female, or Non-binary but can be any string. + """ + gender: String + dateOfBirth: FuzzyDate + dateOfDeath: FuzzyDate + + """The person's age in years""" + age: Int + + """ + [startYear, endYear] (If the 2nd value is not present staff is still active) + """ + yearsActive: [Int] + + """The persons birthplace or hometown""" + homeTown: String + + """The persons blood type""" + bloodType: String + + """ + If the staff member is marked as favourite by the currently authenticated user + """ + isFavourite: Boolean! + + """If the staff member is blocked from being added to favourites""" + isFavouriteBlocked: Boolean! + + """The url for the staff page on the AniList website""" + siteUrl: String + + """Media where the staff member has a production role""" + staffMedia( + sort: [MediaSort] + type: MediaType + onList: Boolean + + """The page""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): MediaConnection + + """Characters voiced by the actor""" + characters( + sort: [CharacterSort] + + """The page""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): CharacterConnection + + """ + Media the actor voiced characters in. (Same data as characters with media as node instead of characters) + """ + characterMedia( + sort: [MediaSort] + onList: Boolean + + """The page""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): MediaConnection + updatedAt: Int @deprecated(reason: "No data available") + + """Staff member that the submission is referencing""" + staff: Staff + + """Submitter for the submission""" + submitter: User + + """Status of the submission""" + submissionStatus: Int + + """Inner details of submission status""" + submissionNotes: String + + """The amount of user's who have favourited the staff member""" + favourites: Int + + """Notes for site moderators""" + modNotes: String +} + +"""The names of the staff member""" +type StaffName { + """The person's given name""" + first: String + + """The person's middle name""" + middle: String + + """The person's surname""" + last: String + + """The person's first and last name""" + full: String + + """The person's full name in their native language""" + native: String + + """Other names the staff member might be referred to as (pen names)""" + alternative: [String] + + """ + The currently authenticated users preferred name language. Default romaji for non-authenticated + """ + userPreferred: String +} + +type StaffImage { + """The person's image of media at its largest size""" + large: String + + """The person's image of media at medium size""" + medium: String +} + +"""Voice actor role for a character""" +type StaffRoleType { + """The voice actors of the character""" + voiceActor: Staff + + """Notes regarding the VA's role for the character""" + roleNotes: String + + """ + Used for grouping roles where multiple dubs exist for the same language. Either dubbing company name or language variant. + """ + dubGroup: String +} + +type StaffConnection { + edges: [StaffEdge] + nodes: [Staff] + + """The pagination information""" + pageInfo: PageInfo +} + +"""Staff connection edge""" +type StaffEdge { + node: Staff + + """The id of the connection""" + id: Int + + """The role of the staff member in the production of the media""" + role: String + + """The order the staff should be displayed from the users favourites""" + favouriteOrder: Int +} + +"""Studio sort enums""" +enum StudioSort { + ID + ID_DESC + NAME + NAME_DESC + SEARCH_MATCH + FAVOURITES + FAVOURITES_DESC +} + +type StudioConnection { + edges: [StudioEdge] + nodes: [Studio] + + """The pagination information""" + pageInfo: PageInfo +} + +"""Studio connection edge""" +type StudioEdge { + node: Studio + + """The id of the connection""" + id: Int + + """If the studio is the main animation studio of the anime""" + isMain: Boolean! + + """The order the character should be displayed from the users favourites""" + favouriteOrder: Int +} + +"""Animation or production company""" +type Studio { + """The id of the studio""" + id: Int! + + """The name of the studio""" + name: String! + + """If the studio is an animation studio or a different kind of company""" + isAnimationStudio: Boolean! + + """The media the studio has worked on""" + media( + """The order the results will be returned in""" + sort: [MediaSort] + + """If the studio was the primary animation studio of the media""" + isMain: Boolean + onList: Boolean + + """The page""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): MediaConnection + + """The url for the studio page on the AniList website""" + siteUrl: String + + """ + If the studio is marked as favourite by the currently authenticated user + """ + isFavourite: Boolean! + + """The amount of user's who have favourited the studio""" + favourites: Int +} + +""" +Media Airing Schedule. NOTE: We only aim to guarantee that FUTURE airing data is present and accurate. +""" +type AiringSchedule { + """The id of the airing schedule item""" + id: Int! + + """The time the episode airs at""" + airingAt: Int! + + """Seconds until episode starts airing""" + timeUntilAiring: Int! + + """The airing episode number""" + episode: Int! + + """The associate media id of the airing episode""" + mediaId: Int! + + """The associate media of the airing episode""" + media: Media +} + +type AiringScheduleConnection { + edges: [AiringScheduleEdge] + nodes: [AiringSchedule] + + """The pagination information""" + pageInfo: PageInfo +} + +"""AiringSchedule connection edge""" +type AiringScheduleEdge { + node: AiringSchedule + + """The id of the connection""" + id: Int +} + +"""Media trend sort enums""" +enum MediaTrendSort { + ID + ID_DESC + MEDIA_ID + MEDIA_ID_DESC + DATE + DATE_DESC + SCORE + SCORE_DESC + POPULARITY + POPULARITY_DESC + TRENDING + TRENDING_DESC + EPISODE + EPISODE_DESC +} + +type MediaTrendConnection { + edges: [MediaTrendEdge] + nodes: [MediaTrend] + + """The pagination information""" + pageInfo: PageInfo +} + +"""Media trend connection edge""" +type MediaTrendEdge { + node: MediaTrend +} + +"""Daily media statistics""" +type MediaTrend { + """The id of the tag""" + mediaId: Int! + + """The day the data was recorded (timestamp)""" + date: Int! + + """The amount of media activity on the day""" + trending: Int! + + """A weighted average score of all the user's scores of the media""" + averageScore: Int + + """The number of users with the media on their list""" + popularity: Int + + """The number of users with watching/reading the media""" + inProgress: Int + + """If the media was being released at this time""" + releasing: Boolean! + + """The episode number of the anime released on this day""" + episode: Int + + """The related media""" + media: Media +} + +"""An external link to another site related to the media or staff member""" +type MediaExternalLink { + """The id of the external link""" + id: Int! + + """The url of the external link or base url of link source""" + url: String + + """The links website site name""" + site: String! + + """The links website site id""" + siteId: Int + type: ExternalLinkType + + """Language the site content is in. See Staff language field for values.""" + language: String + color: String + + """ + The icon image url of the site. Not available for all links. Transparent PNG 64x64 + """ + icon: String + notes: String + isDisabled: Boolean +} + +enum ExternalLinkType { + INFO + STREAMING + SOCIAL +} + +"""Data and links to legal streaming episodes on external sites""" +type MediaStreamingEpisode { + """Title of the episode""" + title: String + + """Url of episode image thumbnail""" + thumbnail: String + + """The url of the episode""" + url: String + + """The site location of the streaming episodes""" + site: String +} + +""" +The ranking of a media in a particular time span and format compared to other media +""" +type MediaRank { + """The id of the rank""" + id: Int! + + """The numerical rank of the media""" + rank: Int! + + """The type of ranking""" + type: MediaRankType! + + """The format the media is ranked within""" + format: MediaFormat! + + """The year the media is ranked within""" + year: Int + + """The season the media is ranked within""" + season: MediaSeason + + """If the ranking is based on all time instead of a season/year""" + allTime: Boolean + + """String that gives context to the ranking type and time span""" + context: String! +} + +"""The type of ranking""" +enum MediaRankType { + """Ranking is based on the media's ratings/score""" + RATED + + """Ranking is based on the media's popularity""" + POPULAR +} + +"""List of anime or manga""" +type MediaList { + """The id of the list entry""" + id: Int! + + """The id of the user owner of the list entry""" + userId: Int! + + """The id of the media""" + mediaId: Int! + + """The watching/reading status""" + status: MediaListStatus + + """The score of the entry""" + score( + """Force the score to be returned in the provided format type.""" + format: ScoreFormat + ): Float + + """The amount of episodes/chapters consumed by the user""" + progress: Int + + """The amount of volumes read by the user""" + progressVolumes: Int + + """The amount of times the user has rewatched/read the media""" + repeat: Int + + """Priority of planning""" + priority: Int + + """If the entry should only be visible to authenticated user""" + private: Boolean + + """Text notes""" + notes: String + + """If the entry shown be hidden from non-custom lists""" + hiddenFromStatusLists: Boolean + + """Map of booleans for which custom lists the entry are in""" + customLists( + """Change return structure to an array of objects""" + asArray: Boolean + ): Json + + """Map of advanced scores with name keys""" + advancedScores: Json + + """When the entry was started by the user""" + startedAt: FuzzyDate + + """When the entry was completed by the user""" + completedAt: FuzzyDate + + """When the entry data was last updated""" + updatedAt: Int + + """When the entry data was created""" + createdAt: Int + media: Media + user: User +} + +"""Review sort enums""" +enum ReviewSort { + ID + ID_DESC + SCORE + SCORE_DESC + RATING + RATING_DESC + CREATED_AT + CREATED_AT_DESC + UPDATED_AT + UPDATED_AT_DESC +} + +type ReviewConnection { + edges: [ReviewEdge] + nodes: [Review] + + """The pagination information""" + pageInfo: PageInfo +} + +"""Review connection edge""" +type ReviewEdge { + node: Review +} + +"""A Review that features in an anime or manga""" +type Review { + """The id of the review""" + id: Int! + + """The id of the review's creator""" + userId: Int! + + """The id of the review's media""" + mediaId: Int! + + """For which type of media the review is for""" + mediaType: MediaType + + """A short summary of the review""" + summary: String + + """The main review body text""" + body( + """Return the string in pre-parsed html instead of markdown""" + asHtml: Boolean + ): String + + """The total user rating of the review""" + rating: Int + + """The amount of user ratings of the review""" + ratingAmount: Int + + """The rating of the review by currently authenticated user""" + userRating: ReviewRating + + """The review score of the media""" + score: Int + + """ + If the review is not yet publicly published and is only viewable by creator + """ + private: Boolean + + """The url for the review page on the AniList website""" + siteUrl: String + + """The time of the thread creation""" + createdAt: Int! + + """The time of the thread last update""" + updatedAt: Int! + + """The creator of the review""" + user: User + + """The media the review is of""" + media: Media +} + +"""Review rating enums""" +enum ReviewRating { + NO_VOTE + UP_VOTE + DOWN_VOTE +} + +"""Recommendation sort enums""" +enum RecommendationSort { + ID + ID_DESC + RATING + RATING_DESC +} + +type RecommendationConnection { + edges: [RecommendationEdge] + nodes: [Recommendation] + + """The pagination information""" + pageInfo: PageInfo +} + +"""Recommendation connection edge""" +type RecommendationEdge { + node: Recommendation +} + +"""Media recommendation""" +type Recommendation { + """The id of the recommendation""" + id: Int! + + """Users rating of the recommendation""" + rating: Int + + """The rating of the recommendation by currently authenticated user""" + userRating: RecommendationRating + + """The media the recommendation is from""" + media: Media + + """The recommended media""" + mediaRecommendation: Media + + """The user that first created the recommendation""" + user: User +} + +"""Recommendation rating enums""" +enum RecommendationRating { + NO_RATING + RATE_UP + RATE_DOWN +} + +"""A media's statistics""" +type MediaStats { + scoreDistribution: [ScoreDistribution] + statusDistribution: [StatusDistribution] + airingProgression: [AiringProgression] @deprecated(reason: "Replaced by MediaTrends") +} + +"""A user's list score distribution.""" +type ScoreDistribution { + score: Int + + """The amount of list entries with this score""" + amount: Int +} + +""" +The distribution of the watching/reading status of media or a user's list +""" +type StatusDistribution { + """The day the activity took place (Unix timestamp)""" + status: MediaListStatus + + """The amount of entries with this status""" + amount: Int +} + +"""Score & Watcher stats for airing anime by episode and mid-week""" +type AiringProgression { + """ + The episode the stats were recorded at. .5 is the mid point between 2 episodes airing dates. + """ + episode: Float + + """The average score for the media""" + score: Float + + """The amount of users watching the anime""" + watching: Int +} + +"""Type of relation media has to its parent.""" +enum MediaRelation { + """An adaption of this media into a different format""" + ADAPTATION + + """Released before the relation""" + PREQUEL + + """Released after the relation""" + SEQUEL + + """The media a side story is from""" + PARENT + + """A side story of the parent media""" + SIDE_STORY + + """Shares at least 1 character""" + CHARACTER + + """A shortened and summarized version""" + SUMMARY + + """An alternative version of the same media""" + ALTERNATIVE + + """An alternative version of the media with a different primary focus""" + SPIN_OFF + + """Other""" + OTHER + + """Version 2 only. The source material the media was adapted from""" + SOURCE + + """Version 2 only.""" + COMPILATION + + """Version 2 only.""" + CONTAINS +} + +type UserStatisticTypes { + anime: UserStatistics + manga: UserStatistics +} + +type UserStatistics { + count: Int! + meanScore: Float! + standardDeviation: Float! + minutesWatched: Int! + episodesWatched: Int! + chaptersRead: Int! + volumesRead: Int! + formats(limit: Int, sort: [UserStatisticsSort]): [UserFormatStatistic] + statuses(limit: Int, sort: [UserStatisticsSort]): [UserStatusStatistic] + scores(limit: Int, sort: [UserStatisticsSort]): [UserScoreStatistic] + lengths(limit: Int, sort: [UserStatisticsSort]): [UserLengthStatistic] + releaseYears(limit: Int, sort: [UserStatisticsSort]): [UserReleaseYearStatistic] + startYears(limit: Int, sort: [UserStatisticsSort]): [UserStartYearStatistic] + genres(limit: Int, sort: [UserStatisticsSort]): [UserGenreStatistic] + tags(limit: Int, sort: [UserStatisticsSort]): [UserTagStatistic] + countries(limit: Int, sort: [UserStatisticsSort]): [UserCountryStatistic] + voiceActors(limit: Int, sort: [UserStatisticsSort]): [UserVoiceActorStatistic] + staff(limit: Int, sort: [UserStatisticsSort]): [UserStaffStatistic] + studios(limit: Int, sort: [UserStatisticsSort]): [UserStudioStatistic] +} + +"""User statistics sort enum""" +enum UserStatisticsSort { + ID + ID_DESC + COUNT + COUNT_DESC + PROGRESS + PROGRESS_DESC + MEAN_SCORE + MEAN_SCORE_DESC +} + +type UserFormatStatistic { + count: Int! + meanScore: Float! + minutesWatched: Int! + chaptersRead: Int! + mediaIds: [Int]! + format: MediaFormat +} + +type UserStatusStatistic { + count: Int! + meanScore: Float! + minutesWatched: Int! + chaptersRead: Int! + mediaIds: [Int]! + status: MediaListStatus +} + +type UserScoreStatistic { + count: Int! + meanScore: Float! + minutesWatched: Int! + chaptersRead: Int! + mediaIds: [Int]! + score: Int +} + +type UserLengthStatistic { + count: Int! + meanScore: Float! + minutesWatched: Int! + chaptersRead: Int! + mediaIds: [Int]! + length: String +} + +type UserReleaseYearStatistic { + count: Int! + meanScore: Float! + minutesWatched: Int! + chaptersRead: Int! + mediaIds: [Int]! + releaseYear: Int +} + +type UserStartYearStatistic { + count: Int! + meanScore: Float! + minutesWatched: Int! + chaptersRead: Int! + mediaIds: [Int]! + startYear: Int +} + +type UserGenreStatistic { + count: Int! + meanScore: Float! + minutesWatched: Int! + chaptersRead: Int! + mediaIds: [Int]! + genre: String +} + +type UserTagStatistic { + count: Int! + meanScore: Float! + minutesWatched: Int! + chaptersRead: Int! + mediaIds: [Int]! + tag: MediaTag +} + +type UserCountryStatistic { + count: Int! + meanScore: Float! + minutesWatched: Int! + chaptersRead: Int! + mediaIds: [Int]! + country: CountryCode +} + +type UserVoiceActorStatistic { + count: Int! + meanScore: Float! + minutesWatched: Int! + chaptersRead: Int! + mediaIds: [Int]! + voiceActor: Staff + characterIds: [Int]! +} + +type UserStaffStatistic { + count: Int! + meanScore: Float! + minutesWatched: Int! + chaptersRead: Int! + mediaIds: [Int]! + staff: Staff +} + +type UserStudioStatistic { + count: Int! + meanScore: Float! + minutesWatched: Int! + chaptersRead: Int! + mediaIds: [Int]! + studio: Studio +} + +"""Mod role enums""" +enum ModRole { + """An AniList administrator""" + ADMIN + + """A head developer of AniList""" + LEAD_DEVELOPER + + """An AniList developer""" + DEVELOPER + + """A lead community moderator""" + LEAD_COMMUNITY + + """A community moderator""" + COMMUNITY + + """A discord community moderator""" + DISCORD_COMMUNITY + + """A lead anime data moderator""" + LEAD_ANIME_DATA + + """An anime data moderator""" + ANIME_DATA + + """A lead manga data moderator""" + LEAD_MANGA_DATA + + """A manga data moderator""" + MANGA_DATA + + """A lead social media moderator""" + LEAD_SOCIAL_MEDIA + + """A social media moderator""" + SOCIAL_MEDIA + + """A retired moderator""" + RETIRED + + """A character data moderator""" + CHARACTER_DATA + + """A staff data moderator""" + STAFF_DATA +} + +"""A user's statistics""" +type UserStats { + """The amount of anime the user has watched in minutes""" + watchedTime: Int + + """The amount of manga chapters the user has read""" + chaptersRead: Int + activityHistory: [UserActivityHistory] + animeStatusDistribution: [StatusDistribution] + mangaStatusDistribution: [StatusDistribution] + animeScoreDistribution: [ScoreDistribution] + mangaScoreDistribution: [ScoreDistribution] + animeListScores: ListScoreStats + mangaListScores: ListScoreStats + favouredGenresOverview: [GenreStats] + favouredGenres: [GenreStats] + favouredTags: [TagStats] + favouredActors: [StaffStats] + favouredStaff: [StaffStats] + favouredStudios: [StudioStats] + favouredYears: [YearStats] + favouredFormats: [FormatStats] +} + +"""A user's activity history stats.""" +type UserActivityHistory { + """The day the activity took place (Unix timestamp)""" + date: Int + + """The amount of activity on the day""" + amount: Int + + """The level of activity represented on a 1-10 scale""" + level: Int +} + +"""User's list score statistics""" +type ListScoreStats { + meanScore: Int + standardDeviation: Int +} + +"""User's genre statistics""" +type GenreStats { + genre: String + amount: Int + meanScore: Int + + """The amount of time in minutes the genre has been watched by the user""" + timeWatched: Int +} + +"""User's tag statistics""" +type TagStats { + tag: MediaTag + amount: Int + meanScore: Int + + """The amount of time in minutes the tag has been watched by the user""" + timeWatched: Int +} + +"""User's staff statistics""" +type StaffStats { + staff: Staff + amount: Int + meanScore: Int + + """ + The amount of time in minutes the staff member has been watched by the user + """ + timeWatched: Int +} + +"""User's studio statistics""" +type StudioStats { + studio: Studio + amount: Int + meanScore: Int + + """ + The amount of time in minutes the studio's works have been watched by the user + """ + timeWatched: Int +} + +"""User's year statistics""" +type YearStats { + year: Int + amount: Int + meanScore: Int +} + +"""User's format statistics""" +type FormatStats { + format: MediaFormat + amount: Int +} + +"""A user's previous name""" +type UserPreviousName { + """A previous name of the user.""" + name: String + + """When the user first changed from this name.""" + createdAt: Int + + """When the user most recently changed from this name.""" + updatedAt: Int +} + +""" +8 digit long date integer (YYYYMMDD). Unknown dates represented by 0. E.g. 2016: 20160000, May 1976: 19760500 +""" +scalar FuzzyDateInt + +"""Media list sort enums""" +enum MediaListSort { + MEDIA_ID + MEDIA_ID_DESC + SCORE + SCORE_DESC + STATUS + STATUS_DESC + PROGRESS + PROGRESS_DESC + PROGRESS_VOLUMES + PROGRESS_VOLUMES_DESC + REPEAT + REPEAT_DESC + PRIORITY + PRIORITY_DESC + STARTED_ON + STARTED_ON_DESC + FINISHED_ON + FINISHED_ON_DESC + ADDED_TIME + ADDED_TIME_DESC + UPDATED_TIME + UPDATED_TIME_DESC + MEDIA_TITLE_ROMAJI + MEDIA_TITLE_ROMAJI_DESC + MEDIA_TITLE_ENGLISH + MEDIA_TITLE_ENGLISH_DESC + MEDIA_TITLE_NATIVE + MEDIA_TITLE_NATIVE_DESC + MEDIA_POPULARITY + MEDIA_POPULARITY_DESC +} + +"""Airing schedule sort enums""" +enum AiringSort { + ID + ID_DESC + MEDIA_ID + MEDIA_ID_DESC + TIME + TIME_DESC + EPISODE + EPISODE_DESC +} + +"""Notification union type""" +union NotificationUnion = AiringNotification | FollowingNotification | ActivityMessageNotification | ActivityMentionNotification | ActivityReplyNotification | ActivityReplySubscribedNotification | ActivityLikeNotification | ActivityReplyLikeNotification | ThreadCommentMentionNotification | ThreadCommentReplyNotification | ThreadCommentSubscribedNotification | ThreadCommentLikeNotification | ThreadLikeNotification | RelatedMediaAdditionNotification | MediaDataChangeNotification | MediaMergeNotification | MediaDeletionNotification + +"""Notification for when an episode of anime airs""" +type AiringNotification { + """The id of the Notification""" + id: Int! + + """The type of notification""" + type: NotificationType + + """The id of the aired anime""" + animeId: Int! + + """The episode number that just aired""" + episode: Int! + + """The notification context text""" + contexts: [String] + + """The time the notification was created at""" + createdAt: Int + + """The associated media of the airing schedule""" + media: Media +} + +""" +Notification for when the authenticated user is followed by another user +""" +type FollowingNotification { + """The id of the Notification""" + id: Int! + + """The id of the user who followed the authenticated user""" + userId: Int! + + """The type of notification""" + type: NotificationType + + """The notification context text""" + context: String + + """The time the notification was created at""" + createdAt: Int + + """The liked activity""" + user: User +} + +"""Notification for when a user is send an activity message""" +type ActivityMessageNotification { + """The id of the Notification""" + id: Int! + + """The if of the user who send the message""" + userId: Int! + + """The type of notification""" + type: NotificationType + + """The id of the activity message""" + activityId: Int! + + """The notification context text""" + context: String + + """The time the notification was created at""" + createdAt: Int + + """The message activity""" + message: MessageActivity + + """The user who sent the message""" + user: User +} + +"""User message activity""" +type MessageActivity { + """The id of the activity""" + id: Int! + + """The user id of the activity's recipient""" + recipientId: Int + + """The user id of the activity's sender""" + messengerId: Int + + """The type of the activity""" + type: ActivityType + + """The number of activity replies""" + replyCount: Int! + + """The message text (Markdown)""" + message( + """Return the string in pre-parsed html instead of markdown""" + asHtml: Boolean + ): String + + """If the activity is locked and can receive replies""" + isLocked: Boolean + + """If the currently authenticated user is subscribed to the activity""" + isSubscribed: Boolean + + """The amount of likes the activity has""" + likeCount: Int! + + """If the currently authenticated user liked the activity""" + isLiked: Boolean + + """ + If the message is private and only viewable to the sender and recipients + """ + isPrivate: Boolean + + """The url for the activity page on the AniList website""" + siteUrl: String + + """The time the activity was created at""" + createdAt: Int! + + """The user who the activity message was sent to""" + recipient: User + + """The user who sent the activity message""" + messenger: User + + """The written replies to the activity""" + replies: [ActivityReply] + + """The users who liked the activity""" + likes: [User] +} + +"""Activity type enum.""" +enum ActivityType { + """A text activity""" + TEXT + + """A anime list update activity""" + ANIME_LIST + + """A manga list update activity""" + MANGA_LIST + + """A text message activity sent to another user""" + MESSAGE + + """Anime & Manga list update, only used in query arguments""" + MEDIA_LIST +} + +"""Replay to an activity item""" +type ActivityReply { + """The id of the reply""" + id: Int! + + """The id of the replies creator""" + userId: Int + + """The id of the parent activity""" + activityId: Int + + """The reply text""" + text( + """Return the string in pre-parsed html instead of markdown""" + asHtml: Boolean + ): String + + """The amount of likes the reply has""" + likeCount: Int! + + """If the currently authenticated user liked the reply""" + isLiked: Boolean + + """The time the reply was created at""" + createdAt: Int! + + """The user who created reply""" + user: User + + """The users who liked the reply""" + likes: [User] +} + +""" +Notification for when authenticated user is @ mentioned in activity or reply +""" +type ActivityMentionNotification { + """The id of the Notification""" + id: Int! + + """The id of the user who mentioned the authenticated user""" + userId: Int! + + """The type of notification""" + type: NotificationType + + """The id of the activity where mentioned""" + activityId: Int! + + """The notification context text""" + context: String + + """The time the notification was created at""" + createdAt: Int + + """The liked activity""" + activity: ActivityUnion + + """The user who mentioned the authenticated user""" + user: User +} + +"""Activity union type""" +union ActivityUnion = TextActivity | ListActivity | MessageActivity + +"""User text activity""" +type TextActivity { + """The id of the activity""" + id: Int! + + """The user id of the activity's creator""" + userId: Int + + """The type of activity""" + type: ActivityType + + """The number of activity replies""" + replyCount: Int! + + """The status text (Markdown)""" + text( + """Return the string in pre-parsed html instead of markdown""" + asHtml: Boolean + ): String + + """The url for the activity page on the AniList website""" + siteUrl: String + + """If the activity is locked and can receive replies""" + isLocked: Boolean + + """If the currently authenticated user is subscribed to the activity""" + isSubscribed: Boolean + + """The amount of likes the activity has""" + likeCount: Int! + + """If the currently authenticated user liked the activity""" + isLiked: Boolean + + """If the activity is pinned to the top of the users activity feed""" + isPinned: Boolean + + """The time the activity was created at""" + createdAt: Int! + + """The user who created the activity""" + user: User + + """The written replies to the activity""" + replies: [ActivityReply] + + """The users who liked the activity""" + likes: [User] +} + +"""User list activity (anime & manga updates)""" +type ListActivity { + """The id of the activity""" + id: Int! + + """The user id of the activity's creator""" + userId: Int + + """The type of activity""" + type: ActivityType + + """The number of activity replies""" + replyCount: Int! + + """The list item's textual status""" + status: String + + """The list progress made""" + progress: String + + """If the activity is locked and can receive replies""" + isLocked: Boolean + + """If the currently authenticated user is subscribed to the activity""" + isSubscribed: Boolean + + """The amount of likes the activity has""" + likeCount: Int! + + """If the currently authenticated user liked the activity""" + isLiked: Boolean + + """If the activity is pinned to the top of the users activity feed""" + isPinned: Boolean + + """The url for the activity page on the AniList website""" + siteUrl: String + + """The time the activity was created at""" + createdAt: Int! + + """The owner of the activity""" + user: User + + """The associated media to the activity update""" + media: Media + + """The written replies to the activity""" + replies: [ActivityReply] + + """The users who liked the activity""" + likes: [User] +} + +""" +Notification for when a user replies to the authenticated users activity +""" +type ActivityReplyNotification { + """The id of the Notification""" + id: Int! + + """The id of the user who replied to the activity""" + userId: Int! + + """The type of notification""" + type: NotificationType + + """The id of the activity which was replied too""" + activityId: Int! + + """The notification context text""" + context: String + + """The time the notification was created at""" + createdAt: Int + + """The liked activity""" + activity: ActivityUnion + + """The user who replied to the activity""" + user: User +} + +""" +Notification for when a user replies to activity the authenticated user has replied to +""" +type ActivityReplySubscribedNotification { + """The id of the Notification""" + id: Int! + + """The id of the user who replied to the activity""" + userId: Int! + + """The type of notification""" + type: NotificationType + + """The id of the activity which was replied too""" + activityId: Int! + + """The notification context text""" + context: String + + """The time the notification was created at""" + createdAt: Int + + """The liked activity""" + activity: ActivityUnion + + """The user who replied to the activity""" + user: User +} + +"""Notification for when a activity is liked""" +type ActivityLikeNotification { + """The id of the Notification""" + id: Int! + + """The id of the user who liked to the activity""" + userId: Int! + + """The type of notification""" + type: NotificationType + + """The id of the activity which was liked""" + activityId: Int! + + """The notification context text""" + context: String + + """The time the notification was created at""" + createdAt: Int + + """The liked activity""" + activity: ActivityUnion + + """The user who liked the activity""" + user: User +} + +"""Notification for when a activity reply is liked""" +type ActivityReplyLikeNotification { + """The id of the Notification""" + id: Int! + + """The id of the user who liked to the activity reply""" + userId: Int! + + """The type of notification""" + type: NotificationType + + """The id of the activity where the reply which was liked""" + activityId: Int! + + """The notification context text""" + context: String + + """The time the notification was created at""" + createdAt: Int + + """The liked activity""" + activity: ActivityUnion + + """The user who liked the activity reply""" + user: User +} + +""" +Notification for when authenticated user is @ mentioned in a forum thread comment +""" +type ThreadCommentMentionNotification { + """The id of the Notification""" + id: Int! + + """The id of the user who mentioned the authenticated user""" + userId: Int! + + """The type of notification""" + type: NotificationType + + """The id of the comment where mentioned""" + commentId: Int! + + """The notification context text""" + context: String + + """The time the notification was created at""" + createdAt: Int + + """The thread that the relevant comment belongs to""" + thread: Thread + + """The thread comment that included the @ mention""" + comment: ThreadComment + + """The user who mentioned the authenticated user""" + user: User +} + +"""Forum Thread""" +type Thread { + """The id of the thread""" + id: Int! + + """The title of the thread""" + title: String + + """The text body of the thread (Markdown)""" + body( + """Return the string in pre-parsed html instead of markdown""" + asHtml: Boolean + ): String + + """The id of the thread owner user""" + userId: Int! + + """The id of the user who most recently commented on the thread""" + replyUserId: Int + + """The id of the most recent comment on the thread""" + replyCommentId: Int + + """The number of comments on the thread""" + replyCount: Int + + """The number of times users have viewed the thread""" + viewCount: Int + + """If the thread is locked and can receive comments""" + isLocked: Boolean + + """ + If the thread is stickied and should be displayed at the top of the page + """ + isSticky: Boolean + + """If the currently authenticated user is subscribed to the thread""" + isSubscribed: Boolean + + """The amount of likes the thread has""" + likeCount: Int! + + """If the currently authenticated user liked the thread""" + isLiked: Boolean + + """The time of the last reply""" + repliedAt: Int + + """The time of the thread creation""" + createdAt: Int! + + """The time of the thread last update""" + updatedAt: Int! + + """The owner of the thread""" + user: User + + """The user to last reply to the thread""" + replyUser: User + + """The users who liked the thread""" + likes: [User] + + """The url for the thread page on the AniList website""" + siteUrl: String + + """The categories of the thread""" + categories: [ThreadCategory] + + """The media categories of the thread""" + mediaCategories: [Media] +} + +"""A forum thread category""" +type ThreadCategory { + """The id of the category""" + id: Int! + + """The name of the category""" + name: String! +} + +"""Forum Thread Comment""" +type ThreadComment { + """The id of the comment""" + id: Int! + + """The user id of the comment's owner""" + userId: Int + + """The id of thread the comment belongs to""" + threadId: Int + + """The text content of the comment (Markdown)""" + comment( + """Return the string in pre-parsed html instead of markdown""" + asHtml: Boolean + ): String + + """The amount of likes the comment has""" + likeCount: Int! + + """If the currently authenticated user liked the comment""" + isLiked: Boolean + + """The url for the comment page on the AniList website""" + siteUrl: String + + """The time of the comments creation""" + createdAt: Int! + + """The time of the comments last update""" + updatedAt: Int! + + """The thread the comment belongs to""" + thread: Thread + + """The user who created the comment""" + user: User + + """The users who liked the comment""" + likes: [User] + + """The comment's child reply comments""" + childComments: Json + + """If the comment tree is locked and may not receive replies or edits""" + isLocked: Boolean +} + +"""Notification for when a user replies to your forum thread comment""" +type ThreadCommentReplyNotification { + """The id of the Notification""" + id: Int! + + """The id of the user who create the comment reply""" + userId: Int! + + """The type of notification""" + type: NotificationType + + """The id of the reply comment""" + commentId: Int! + + """The notification context text""" + context: String + + """The time the notification was created at""" + createdAt: Int + + """The thread that the relevant comment belongs to""" + thread: Thread + + """The reply thread comment""" + comment: ThreadComment + + """The user who replied to the activity""" + user: User +} + +"""Notification for when a user replies to a subscribed forum thread""" +type ThreadCommentSubscribedNotification { + """The id of the Notification""" + id: Int! + + """The id of the user who commented on the thread""" + userId: Int! + + """The type of notification""" + type: NotificationType + + """The id of the new comment in the subscribed thread""" + commentId: Int! + + """The notification context text""" + context: String + + """The time the notification was created at""" + createdAt: Int + + """The thread that the relevant comment belongs to""" + thread: Thread + + """The reply thread comment""" + comment: ThreadComment + + """The user who replied to the subscribed thread""" + user: User +} + +"""Notification for when a thread comment is liked""" +type ThreadCommentLikeNotification { + """The id of the Notification""" + id: Int! + + """The id of the user who liked to the activity""" + userId: Int! + + """The type of notification""" + type: NotificationType + + """The id of the activity which was liked""" + commentId: Int! + + """The notification context text""" + context: String + + """The time the notification was created at""" + createdAt: Int + + """The thread that the relevant comment belongs to""" + thread: Thread + + """The thread comment that was liked""" + comment: ThreadComment + + """The user who liked the activity""" + user: User +} + +"""Notification for when a thread is liked""" +type ThreadLikeNotification { + """The id of the Notification""" + id: Int! + + """The id of the user who liked to the activity""" + userId: Int! + + """The type of notification""" + type: NotificationType + + """The id of the thread which was liked""" + threadId: Int! + + """The notification context text""" + context: String + + """The time the notification was created at""" + createdAt: Int + + """The thread that the relevant comment belongs to""" + thread: Thread + + """The liked thread comment""" + comment: ThreadComment + + """The user who liked the activity""" + user: User +} + +"""Notification for when new media is added to the site""" +type RelatedMediaAdditionNotification { + """The id of the Notification""" + id: Int! + + """The type of notification""" + type: NotificationType + + """The id of the new media""" + mediaId: Int! + + """The notification context text""" + context: String + + """The time the notification was created at""" + createdAt: Int + + """The associated media of the airing schedule""" + media: Media +} + +""" +Notification for when a media entry's data was changed in a significant way impacting users' list tracking +""" +type MediaDataChangeNotification { + """The id of the Notification""" + id: Int! + + """The type of notification""" + type: NotificationType + + """The id of the media that received data changes""" + mediaId: Int! + + """The reason for the media data change""" + context: String + + """The reason for the media data change""" + reason: String + + """The time the notification was created at""" + createdAt: Int + + """The media that received data changes""" + media: Media +} + +""" +Notification for when a media entry is merged into another for a user who had it on their list +""" +type MediaMergeNotification { + """The id of the Notification""" + id: Int! + + """The type of notification""" + type: NotificationType + + """The id of the media that was merged into""" + mediaId: Int! + + """The title of the deleted media""" + deletedMediaTitles: [String] + + """The reason for the media data change""" + context: String + + """The reason for the media merge""" + reason: String + + """The time the notification was created at""" + createdAt: Int + + """The media that was merged into""" + media: Media +} + +""" +Notification for when a media tracked in a user's list is deleted from the site +""" +type MediaDeletionNotification { + """The id of the Notification""" + id: Int! + + """The type of notification""" + type: NotificationType + + """The title of the deleted media""" + deletedMediaTitle: String + + """The reason for the media deletion""" + context: String + + """The reason for the media deletion""" + reason: String + + """The time the notification was created at""" + createdAt: Int +} + +"""Activity sort enums""" +enum ActivitySort { + ID + ID_DESC + PINNED +} + +"""Thread sort enums""" +enum ThreadSort { + ID + ID_DESC + TITLE + TITLE_DESC + CREATED_AT + CREATED_AT_DESC + UPDATED_AT + UPDATED_AT_DESC + REPLIED_AT + REPLIED_AT_DESC + REPLY_COUNT + REPLY_COUNT_DESC + VIEW_COUNT + VIEW_COUNT_DESC + IS_STICKY + SEARCH_MATCH +} + +"""Thread comments sort enums""" +enum ThreadCommentSort { + ID + ID_DESC +} + +"""Types that can be liked""" +enum LikeableType { + THREAD + THREAD_COMMENT + ACTIVITY + ACTIVITY_REPLY +} + +"""List of anime or manga""" +type MediaListCollection { + """Grouped media list entries""" + lists: [MediaListGroup] + + """The owner of the list""" + user: User + + """If there is another chunk""" + hasNextChunk: Boolean + + """A map of media list entry arrays grouped by status""" + statusLists(asArray: Boolean): [[MediaList]] @deprecated(reason: "Not GraphQL spec compliant, use lists field instead.") + + """A map of media list entry arrays grouped by custom lists""" + customLists(asArray: Boolean): [[MediaList]] @deprecated(reason: "Not GraphQL spec compliant, use lists field instead.") +} + +"""List group of anime or manga entries""" +type MediaListGroup { + """Media list entries""" + entries: [MediaList] + name: String + isCustomList: Boolean + isSplitCompletedList: Boolean + status: MediaListStatus +} + +"""Provides the parsed markdown as html""" +type ParsedMarkdown { + """The parsed markdown as html""" + html: String +} + +type AniChartUser { + user: User + settings: Json + highlights: Json +} + +type SiteStatistics { + users( + sort: [SiteTrendSort] + + """The page""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): SiteTrendConnection + anime( + sort: [SiteTrendSort] + + """The page""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): SiteTrendConnection + manga( + sort: [SiteTrendSort] + + """The page""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): SiteTrendConnection + characters( + sort: [SiteTrendSort] + + """The page""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): SiteTrendConnection + staff( + sort: [SiteTrendSort] + + """The page""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): SiteTrendConnection + studios( + sort: [SiteTrendSort] + + """The page""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): SiteTrendConnection + reviews( + sort: [SiteTrendSort] + + """The page""" + page: Int + + """The amount of entries per page, max 25""" + perPage: Int + ): SiteTrendConnection +} + +"""Site trend sort enums""" +enum SiteTrendSort { + DATE + DATE_DESC + COUNT + COUNT_DESC + CHANGE + CHANGE_DESC +} + +type SiteTrendConnection { + edges: [SiteTrendEdge] + nodes: [SiteTrend] + + """The pagination information""" + pageInfo: PageInfo +} + +"""Site trend connection edge""" +type SiteTrendEdge { + node: SiteTrend +} + +"""Daily site statistics""" +type SiteTrend { + """The day the data was recorded (timestamp)""" + date: Int! + count: Int! + + """The change from yesterday""" + change: Int! +} + +enum ExternalLinkMediaType { + ANIME + MANGA + STAFF +} + +type Mutation { + UpdateUser( + """User's about/bio text""" + about: String + + """User's title language""" + titleLanguage: UserTitleLanguage + + """If the user should see media marked as adult-only""" + displayAdultContent: Boolean + + """ + If the user should get notifications when a show they are watching aires + """ + airingNotifications: Boolean + + """The user's list scoring system""" + scoreFormat: ScoreFormat + + """The user's default list order""" + rowOrder: String + + """Profile highlight color""" + profileColor: String + + """Profile highlight color""" + donatorBadge: String + + """Notification options""" + notificationOptions: [NotificationOptionInput] + + """Timezone offset format: -?HH:MM""" + timezone: String + + """ + Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always. + """ + activityMergeTime: Int + + """The user's anime list options""" + animeListOptions: MediaListOptionsInput + + """The user's anime list options""" + mangaListOptions: MediaListOptionsInput + + """The language the user wants to see staff and character names in""" + staffNameLanguage: UserStaffNameLanguage + + """Only allow messages from other users the user follows""" + restrictMessagesToFollowing: Boolean + disabledListActivity: [ListActivityOptionInput] + ): User + + """Create or update a media list entry""" + SaveMediaListEntry( + """The list entry id, required for updating""" + id: Int + + """The id of the media the entry is of""" + mediaId: Int + + """The watching/reading status""" + status: MediaListStatus + + """The score of the media in the user's chosen scoring method""" + score: Float + + """The score of the media in 100 point""" + scoreRaw: Int + + """The amount of episodes/chapters consumed by the user""" + progress: Int + + """The amount of volumes read by the user""" + progressVolumes: Int + + """The amount of times the user has rewatched/read the media""" + repeat: Int + + """Priority of planning""" + priority: Int + + """If the entry should only be visible to authenticated user""" + private: Boolean + + """Text notes""" + notes: String + + """If the entry shown be hidden from non-custom lists""" + hiddenFromStatusLists: Boolean + + """Array of custom list names which should be enabled for this entry""" + customLists: [String] + + """Array of advanced scores""" + advancedScores: [Float] + + """When the entry was started by the user""" + startedAt: FuzzyDateInput + + """When the entry was completed by the user""" + completedAt: FuzzyDateInput + ): MediaList + + """Update multiple media list entries to the same values""" + UpdateMediaListEntries( + """The watching/reading status""" + status: MediaListStatus + + """The score of the media in the user's chosen scoring method""" + score: Float + + """The score of the media in 100 point""" + scoreRaw: Int + + """The amount of episodes/chapters consumed by the user""" + progress: Int + + """The amount of volumes read by the user""" + progressVolumes: Int + + """The amount of times the user has rewatched/read the media""" + repeat: Int + + """Priority of planning""" + priority: Int + + """If the entry should only be visible to authenticated user""" + private: Boolean + + """Text notes""" + notes: String + + """If the entry shown be hidden from non-custom lists""" + hiddenFromStatusLists: Boolean + + """Array of advanced scores""" + advancedScores: [Float] + + """When the entry was started by the user""" + startedAt: FuzzyDateInput + + """When the entry was completed by the user""" + completedAt: FuzzyDateInput + + """The list entries ids to update""" + ids: [Int] + ): [MediaList] + + """Delete a media list entry""" + DeleteMediaListEntry( + """The id of the media list entry to delete""" + id: Int + ): Deleted + + """Delete a custom list and remove the list entries from it""" + DeleteCustomList( + """The name of the custom list to delete""" + customList: String + + """The media list type of the custom list""" + type: MediaType + ): Deleted + + """Create or update text activity for the currently authenticated user""" + SaveTextActivity( + """The activity's id, required for updating""" + id: Int + + """The activity text""" + text: String + + """If the activity should be locked. (Mod Only)""" + locked: Boolean + ): TextActivity + + """Create or update message activity for the currently authenticated user""" + SaveMessageActivity( + """The activity id, required for updating""" + id: Int + + """The activity message text""" + message: String + + """The id of the user the message is being sent to""" + recipientId: Int + + """If the activity should be private""" + private: Boolean + + """If the activity should be locked. (Mod Only)""" + locked: Boolean + + """If the message should be sent from the Moderator account (Mod Only)""" + asMod: Boolean + ): MessageActivity + + """Update list activity (Mod Only)""" + SaveListActivity( + """The activity's id, required for updating""" + id: Int + + """If the activity should be locked. (Mod Only)""" + locked: Boolean + ): ListActivity + + """Delete an activity item of the authenticated users""" + DeleteActivity( + """The id of the activity to delete""" + id: Int + ): Deleted + + """Toggle activity to be pinned to the top of the user's activity feed""" + ToggleActivityPin( + """Toggle activity id to be pinned""" + id: Int + + """If the activity should be pinned or unpinned""" + pinned: Boolean + ): ActivityUnion + + """Toggle the subscription of an activity item""" + ToggleActivitySubscription( + """The id of the activity to un/subscribe""" + activityId: Int + + """Whether to subscribe or unsubscribe from the activity""" + subscribe: Boolean + ): ActivityUnion + + """Create or update an activity reply""" + SaveActivityReply( + """The activity reply id, required for updating""" + id: Int + + """The id of the parent activity being replied to""" + activityId: Int + + """The reply text""" + text: String + + """If the reply should be sent from the Moderator account (Mod Only)""" + asMod: Boolean + ): ActivityReply + + """Delete an activity reply of the authenticated users""" + DeleteActivityReply( + """The id of the reply to delete""" + id: Int + ): Deleted + + """ + Add or remove a like from a likeable type. + Returns all the users who liked the same model + """ + ToggleLike( + """The id of the likeable type""" + id: Int + + """The type of model to be un/liked""" + type: LikeableType + ): [User] + + """Add or remove a like from a likeable type.""" + ToggleLikeV2( + """The id of the likeable type""" + id: Int + + """The type of model to be un/liked""" + type: LikeableType + ): LikeableUnion + + """Toggle the un/following of a user""" + ToggleFollow( + """The id of the user to un/follow""" + userId: Int + ): User + + """ + Favourite or unfavourite an anime, manga, character, staff member, or studio + """ + ToggleFavourite( + """The id of the anime to un/favourite""" + animeId: Int + + """The id of the manga to un/favourite""" + mangaId: Int + + """The id of the character to un/favourite""" + characterId: Int + + """The id of the staff to un/favourite""" + staffId: Int + + """The id of the studio to un/favourite""" + studioId: Int + ): Favourites + + """Update the order favourites are displayed in""" + UpdateFavouriteOrder( + """The id of the anime to un/favourite""" + animeIds: [Int] + + """The id of the manga to un/favourite""" + mangaIds: [Int] + + """The id of the character to un/favourite""" + characterIds: [Int] + + """The id of the staff to un/favourite""" + staffIds: [Int] + + """The id of the studio to un/favourite""" + studioIds: [Int] + + """List of integers which the anime should be ordered by (Asc)""" + animeOrder: [Int] + + """List of integers which the manga should be ordered by (Asc)""" + mangaOrder: [Int] + + """List of integers which the character should be ordered by (Asc)""" + characterOrder: [Int] + + """List of integers which the staff should be ordered by (Asc)""" + staffOrder: [Int] + + """List of integers which the studio should be ordered by (Asc)""" + studioOrder: [Int] + ): Favourites + + """Create or update a review""" + SaveReview( + """The review id, required for updating""" + id: Int + + """The id of the media the review is of""" + mediaId: Int + + """The main review text. Min:2200 characters""" + body: String + + """A short summary/preview of the review. Min:20, Max:120 characters""" + summary: String + + """A short summary/preview of the review. Min:20, Max:120 characters""" + score: Int + + """If the review should only be visible to its creator""" + private: Boolean + ): Review + + """Delete a review""" + DeleteReview( + """The id of the review to delete""" + id: Int + ): Deleted + + """Rate a review""" + RateReview( + """The id of the review to rate""" + reviewId: Int + + """The rating to apply to the review""" + rating: ReviewRating + ): Review + + """Recommendation a media""" + SaveRecommendation( + """The id of the base media""" + mediaId: Int + + """The id of the media to recommend""" + mediaRecommendationId: Int + + """The rating to give the recommendation""" + rating: RecommendationRating + ): Recommendation + + """Create or update a forum thread""" + SaveThread( + """The thread id, required for updating""" + id: Int + + """The title of the thread""" + title: String + + """The main text body of the thread""" + body: String + + """Forum categories the thread should be within""" + categories: [Int] + + """Media related to the contents of the thread""" + mediaCategories: [Int] + + """If the thread should be stickied. (Mod Only)""" + sticky: Boolean + + """If the thread should be locked. (Mod Only)""" + locked: Boolean + ): Thread + + """Delete a thread""" + DeleteThread( + """The id of the thread to delete""" + id: Int + ): Deleted + + """Toggle the subscription of a forum thread""" + ToggleThreadSubscription( + """The id of the forum thread to un/subscribe""" + threadId: Int + + """Whether to subscribe or unsubscribe from the forum thread""" + subscribe: Boolean + ): Thread + + """Create or update a thread comment""" + SaveThreadComment( + """The comment id, required for updating""" + id: Int + + """The id of thread the comment belongs to""" + threadId: Int + + """The id of thread comment to reply to""" + parentCommentId: Int + + """The comment markdown text""" + comment: String + + """If the comment tree should be locked. (Mod Only)""" + locked: Boolean + ): ThreadComment + + """Delete a thread comment""" + DeleteThreadComment( + """The id of the thread comment to delete""" + id: Int + ): Deleted + UpdateAniChartSettings(titleLanguage: String, outgoingLinkProvider: String, theme: String, sort: String): Json + UpdateAniChartHighlights(highlights: [AniChartHighlightInput]): Json +} + +"""Notification option input""" +input NotificationOptionInput { + """The type of notification""" + type: NotificationType + + """Whether this type of notification is enabled""" + enabled: Boolean +} + +"""A user's list options for anime or manga lists""" +input MediaListOptionsInput { + """The order each list should be displayed in""" + sectionOrder: [String] + + """If the completed sections of the list should be separated by format""" + splitCompletedSectionByFormat: Boolean + + """The names of the user's custom lists""" + customLists: [String] + + """The names of the user's advanced scoring sections""" + advancedScoring: [String] + + """If advanced scoring is enabled""" + advancedScoringEnabled: Boolean + + """list theme""" + theme: String +} + +input ListActivityOptionInput { + disabled: Boolean + type: MediaListStatus +} + +"""Date object that allows for incomplete date values (fuzzy)""" +input FuzzyDateInput { + """Numeric Year (2017)""" + year: Int + + """Numeric Month (3)""" + month: Int + + """Numeric Day (24)""" + day: Int +} + +"""Deleted data type""" +type Deleted { + """If an item has been successfully deleted""" + deleted: Boolean +} + +"""Likeable union type""" +union LikeableUnion = ListActivity | TextActivity | MessageActivity | ActivityReply | Thread | ThreadComment + +input AniChartHighlightInput { + mediaId: Int + highlight: String +} + +"""Page of data (Used for internal use only)""" +type InternalPage { + mediaSubmissions( + mediaId: Int + submissionId: Int + userId: Int + assigneeId: Int + status: SubmissionStatus + + """Filter by the media's type""" + type: MediaType + + """The order the results will be returned in""" + sort: [SubmissionSort] + ): [MediaSubmission] + characterSubmissions( + characterId: Int + + """Filter by the submitter of the submission""" + userId: Int + assigneeId: Int + + """Filter by the status of the submission""" + status: SubmissionStatus + + """The order the results will be returned in""" + sort: [SubmissionSort] + ): [CharacterSubmission] + staffSubmissions( + staffId: Int + + """Filter by the submitter of the submission""" + userId: Int + assigneeId: Int + + """Filter by the status of the submission""" + status: SubmissionStatus + + """The order the results will be returned in""" + sort: [SubmissionSort] + ): [StaffSubmission] + revisionHistory( + """Filter by the user id""" + userId: Int + + """Filter by the media id""" + mediaId: Int + + """Filter by the character id""" + characterId: Int + + """Filter by the staff id""" + staffId: Int + + """Filter by the studio id""" + studioId: Int + ): [RevisionHistory] + reports(reporterId: Int, reportedId: Int): [Report] + modActions(userId: Int, modId: Int): [ModAction] + userBlockSearch( + """Filter by search query""" + search: String + ): [User] + + """The pagination information""" + pageInfo: PageInfo + users( + """Filter by the user id""" + id: Int + + """Filter by the name of the user""" + name: String + + """Filter to moderators only if true""" + isModerator: Boolean + + """Filter by search query""" + search: String + + """The order the results will be returned in""" + sort: [UserSort] + ): [User] + media( + """Filter by the media id""" + id: Int + + """Filter by the media's MyAnimeList id""" + idMal: Int + + """Filter by the start date of the media""" + startDate: FuzzyDateInt + + """Filter by the end date of the media""" + endDate: FuzzyDateInt + + """Filter by the season the media was released in""" + season: MediaSeason + + """ + The year of the season (Winter 2017 would also include December 2016 releases). Requires season argument + """ + seasonYear: Int + + """Filter by the media's type""" + type: MediaType + + """Filter by the media's format""" + format: MediaFormat + + """Filter by the media's current release status""" + status: MediaStatus + + """Filter by amount of episodes the media has""" + episodes: Int + + """Filter by the media's episode length""" + duration: Int + + """Filter by the media's chapter count""" + chapters: Int + + """Filter by the media's volume count""" + volumes: Int + + """Filter by if the media's intended for 18+ adult audiences""" + isAdult: Boolean + + """Filter by the media's genres""" + genre: String + + """Filter by the media's tags""" + tag: String + + """ + Only apply the tags filter argument to tags above this rank. Default: 18 + """ + minimumTagRank: Int + + """Filter by the media's tags with in a tag category""" + tagCategory: String + + """Filter by the media on the authenticated user's lists""" + onList: Boolean + + """Filter media by sites name with a online streaming or reading license""" + licensedBy: String + + """Filter media by sites id with a online streaming or reading license""" + licensedById: Int + + """Filter by the media's average score""" + averageScore: Int + + """Filter by the number of users with this media on their list""" + popularity: Int + + """Filter by the source type of the media""" + source: MediaSource + + """Filter by the media's country of origin""" + countryOfOrigin: CountryCode + + """If the media is officially licensed or a self-published doujin release""" + isLicensed: Boolean + + """Filter by search query""" + search: String + + """Filter by the media id""" + id_not: Int + + """Filter by the media id""" + id_in: [Int] + + """Filter by the media id""" + id_not_in: [Int] + + """Filter by the media's MyAnimeList id""" + idMal_not: Int + + """Filter by the media's MyAnimeList id""" + idMal_in: [Int] + + """Filter by the media's MyAnimeList id""" + idMal_not_in: [Int] + + """Filter by the start date of the media""" + startDate_greater: FuzzyDateInt + + """Filter by the start date of the media""" + startDate_lesser: FuzzyDateInt + + """Filter by the start date of the media""" + startDate_like: String + + """Filter by the end date of the media""" + endDate_greater: FuzzyDateInt + + """Filter by the end date of the media""" + endDate_lesser: FuzzyDateInt + + """Filter by the end date of the media""" + endDate_like: String + + """Filter by the media's format""" + format_in: [MediaFormat] + + """Filter by the media's format""" + format_not: MediaFormat + + """Filter by the media's format""" + format_not_in: [MediaFormat] + + """Filter by the media's current release status""" + status_in: [MediaStatus] + + """Filter by the media's current release status""" + status_not: MediaStatus + + """Filter by the media's current release status""" + status_not_in: [MediaStatus] + + """Filter by amount of episodes the media has""" + episodes_greater: Int + + """Filter by amount of episodes the media has""" + episodes_lesser: Int + + """Filter by the media's episode length""" + duration_greater: Int + + """Filter by the media's episode length""" + duration_lesser: Int + + """Filter by the media's chapter count""" + chapters_greater: Int + + """Filter by the media's chapter count""" + chapters_lesser: Int + + """Filter by the media's volume count""" + volumes_greater: Int + + """Filter by the media's volume count""" + volumes_lesser: Int + + """Filter by the media's genres""" + genre_in: [String] + + """Filter by the media's genres""" + genre_not_in: [String] + + """Filter by the media's tags""" + tag_in: [String] + + """Filter by the media's tags""" + tag_not_in: [String] + + """Filter by the media's tags with in a tag category""" + tagCategory_in: [String] + + """Filter by the media's tags with in a tag category""" + tagCategory_not_in: [String] + + """Filter media by sites name with a online streaming or reading license""" + licensedBy_in: [String] + + """Filter media by sites id with a online streaming or reading license""" + licensedById_in: [Int] + + """Filter by the media's average score""" + averageScore_not: Int + + """Filter by the media's average score""" + averageScore_greater: Int + + """Filter by the media's average score""" + averageScore_lesser: Int + + """Filter by the number of users with this media on their list""" + popularity_not: Int + + """Filter by the number of users with this media on their list""" + popularity_greater: Int + + """Filter by the number of users with this media on their list""" + popularity_lesser: Int + + """Filter by the source type of the media""" + source_in: [MediaSource] + + """The order the results will be returned in""" + sort: [MediaSort] + ): [Media] + characters( + """Filter by character id""" + id: Int + + """Filter by character by if its their birthday today""" + isBirthday: Boolean + + """Filter by search query""" + search: String + + """Filter by character id""" + id_not: Int + + """Filter by character id""" + id_in: [Int] + + """Filter by character id""" + id_not_in: [Int] + + """The order the results will be returned in""" + sort: [CharacterSort] + ): [Character] + staff( + """Filter by the staff id""" + id: Int + + """Filter by staff by if its their birthday today""" + isBirthday: Boolean + + """Filter by search query""" + search: String + + """Filter by the staff id""" + id_not: Int + + """Filter by the staff id""" + id_in: [Int] + + """Filter by the staff id""" + id_not_in: [Int] + + """The order the results will be returned in""" + sort: [StaffSort] + ): [Staff] + studios( + """Filter by the studio id""" + id: Int + + """Filter by search query""" + search: String + + """Filter by the studio id""" + id_not: Int + + """Filter by the studio id""" + id_in: [Int] + + """Filter by the studio id""" + id_not_in: [Int] + + """The order the results will be returned in""" + sort: [StudioSort] + ): [Studio] + mediaList( + """Filter by a list entry's id""" + id: Int + + """Filter by a user's id""" + userId: Int + + """Filter by a user's name""" + userName: String + + """Filter by the list entries media type""" + type: MediaType + + """Filter by the watching/reading status""" + status: MediaListStatus + + """Filter by the media id of the list entry""" + mediaId: Int + + """ + Filter list entries to users who are being followed by the authenticated user + """ + isFollowing: Boolean + + """Filter by note words and #tags""" + notes: String + + """Filter by the date the user started the media""" + startedAt: FuzzyDateInt + + """Filter by the date the user completed the media""" + completedAt: FuzzyDateInt + + """ + Limit to only entries also on the auth user's list. Requires user id or name arguments. + """ + compareWithAuthList: Boolean + + """Filter by a user's id""" + userId_in: [Int] + + """Filter by the watching/reading status""" + status_in: [MediaListStatus] + + """Filter by the watching/reading status""" + status_not_in: [MediaListStatus] + + """Filter by the watching/reading status""" + status_not: MediaListStatus + + """Filter by the media id of the list entry""" + mediaId_in: [Int] + + """Filter by the media id of the list entry""" + mediaId_not_in: [Int] + + """Filter by note words and #tags""" + notes_like: String + + """Filter by the date the user started the media""" + startedAt_greater: FuzzyDateInt + + """Filter by the date the user started the media""" + startedAt_lesser: FuzzyDateInt + + """Filter by the date the user started the media""" + startedAt_like: String + + """Filter by the date the user completed the media""" + completedAt_greater: FuzzyDateInt + + """Filter by the date the user completed the media""" + completedAt_lesser: FuzzyDateInt + + """Filter by the date the user completed the media""" + completedAt_like: String + + """The order the results will be returned in""" + sort: [MediaListSort] + ): [MediaList] + airingSchedules( + """Filter by the id of the airing schedule item""" + id: Int + + """Filter by the id of associated media""" + mediaId: Int + + """Filter by the airing episode number""" + episode: Int + + """Filter by the time of airing""" + airingAt: Int + + """Filter to episodes that haven't yet aired""" + notYetAired: Boolean + + """Filter by the id of the airing schedule item""" + id_not: Int + + """Filter by the id of the airing schedule item""" + id_in: [Int] + + """Filter by the id of the airing schedule item""" + id_not_in: [Int] + + """Filter by the id of associated media""" + mediaId_not: Int + + """Filter by the id of associated media""" + mediaId_in: [Int] + + """Filter by the id of associated media""" + mediaId_not_in: [Int] + + """Filter by the airing episode number""" + episode_not: Int + + """Filter by the airing episode number""" + episode_in: [Int] + + """Filter by the airing episode number""" + episode_not_in: [Int] + + """Filter by the airing episode number""" + episode_greater: Int + + """Filter by the airing episode number""" + episode_lesser: Int + + """Filter by the time of airing""" + airingAt_greater: Int + + """Filter by the time of airing""" + airingAt_lesser: Int + + """The order the results will be returned in""" + sort: [AiringSort] + ): [AiringSchedule] + mediaTrends( + """Filter by the media id""" + mediaId: Int + + """Filter by date""" + date: Int + + """Filter by trending amount""" + trending: Int + + """Filter by score""" + averageScore: Int + + """Filter by popularity""" + popularity: Int + + """Filter by episode number""" + episode: Int + + """Filter to stats recorded while the media was releasing""" + releasing: Boolean + + """Filter by the media id""" + mediaId_not: Int + + """Filter by the media id""" + mediaId_in: [Int] + + """Filter by the media id""" + mediaId_not_in: [Int] + + """Filter by date""" + date_greater: Int + + """Filter by date""" + date_lesser: Int + + """Filter by trending amount""" + trending_greater: Int + + """Filter by trending amount""" + trending_lesser: Int + + """Filter by trending amount""" + trending_not: Int + + """Filter by score""" + averageScore_greater: Int + + """Filter by score""" + averageScore_lesser: Int + + """Filter by score""" + averageScore_not: Int + + """Filter by popularity""" + popularity_greater: Int + + """Filter by popularity""" + popularity_lesser: Int + + """Filter by popularity""" + popularity_not: Int + + """Filter by episode number""" + episode_greater: Int + + """Filter by episode number""" + episode_lesser: Int + + """Filter by episode number""" + episode_not: Int + + """The order the results will be returned in""" + sort: [MediaTrendSort] + ): [MediaTrend] + notifications( + """Filter by the type of notifications""" + type: NotificationType + + """Reset the unread notification count to 0 on load""" + resetNotificationCount: Boolean + + """Filter by the type of notifications""" + type_in: [NotificationType] + ): [NotificationUnion] + followers( + """User id of the follower/followed""" + userId: Int! + + """The order the results will be returned in""" + sort: [UserSort] + ): [User] + following( + """User id of the follower/followed""" + userId: Int! + + """The order the results will be returned in""" + sort: [UserSort] + ): [User] + activities( + """Filter by the activity id""" + id: Int + + """Filter by the owner user id""" + userId: Int + + """Filter by the id of the user who sent a message""" + messengerId: Int + + """Filter by the associated media id of the activity""" + mediaId: Int + + """Filter by the type of activity""" + type: ActivityType + + """ + Filter activity to users who are being followed by the authenticated user + """ + isFollowing: Boolean + + """Filter activity to only activity with replies""" + hasReplies: Boolean + + """Filter activity to only activity with replies or is of type text""" + hasRepliesOrTypeText: Boolean + + """Filter by the time the activity was created""" + createdAt: Int + + """Filter by the activity id""" + id_not: Int + + """Filter by the activity id""" + id_in: [Int] + + """Filter by the activity id""" + id_not_in: [Int] + + """Filter by the owner user id""" + userId_not: Int + + """Filter by the owner user id""" + userId_in: [Int] + + """Filter by the owner user id""" + userId_not_in: [Int] + + """Filter by the id of the user who sent a message""" + messengerId_not: Int + + """Filter by the id of the user who sent a message""" + messengerId_in: [Int] + + """Filter by the id of the user who sent a message""" + messengerId_not_in: [Int] + + """Filter by the associated media id of the activity""" + mediaId_not: Int + + """Filter by the associated media id of the activity""" + mediaId_in: [Int] + + """Filter by the associated media id of the activity""" + mediaId_not_in: [Int] + + """Filter by the type of activity""" + type_not: ActivityType + + """Filter by the type of activity""" + type_in: [ActivityType] + + """Filter by the type of activity""" + type_not_in: [ActivityType] + + """Filter by the time the activity was created""" + createdAt_greater: Int + + """Filter by the time the activity was created""" + createdAt_lesser: Int + + """The order the results will be returned in""" + sort: [ActivitySort] + ): [ActivityUnion] + activityReplies( + """Filter by the reply id""" + id: Int + + """Filter by the parent id""" + activityId: Int + ): [ActivityReply] + threads( + """Filter by the thread id""" + id: Int + + """Filter by the user id of the thread's creator""" + userId: Int + + """Filter by the user id of the last user to comment on the thread""" + replyUserId: Int + + """Filter by if the currently authenticated user's subscribed threads""" + subscribed: Boolean + + """Filter by thread category id""" + categoryId: Int + + """Filter by thread media id category""" + mediaCategoryId: Int + + """Filter by search query""" + search: String + + """Filter by the thread id""" + id_in: [Int] + + """The order the results will be returned in""" + sort: [ThreadSort] + ): [Thread] + threadComments( + """Filter by the comment id""" + id: Int + + """Filter by the thread id""" + threadId: Int + + """Filter by the user id of the comment's creator""" + userId: Int + + """The order the results will be returned in""" + sort: [ThreadCommentSort] + ): [ThreadComment] + reviews( + """Filter by Review id""" + id: Int + + """Filter by media id""" + mediaId: Int + + """Filter by user id""" + userId: Int + + """Filter by media type""" + mediaType: MediaType + + """The order the results will be returned in""" + sort: [ReviewSort] + ): [Review] + recommendations( + """Filter by recommendation id""" + id: Int + + """Filter by media id""" + mediaId: Int + + """Filter by media recommendation id""" + mediaRecommendationId: Int + + """Filter by user who created the recommendation""" + userId: Int + + """Filter by total rating of the recommendation""" + rating: Int + + """Filter by the media on the authenticated user's lists""" + onList: Boolean + + """Filter by total rating of the recommendation""" + rating_greater: Int + + """Filter by total rating of the recommendation""" + rating_lesser: Int + + """The order the results will be returned in""" + sort: [RecommendationSort] + ): [Recommendation] + likes( + """The id of the likeable type""" + likeableId: Int + + """The type of model the id applies to""" + type: LikeableType + ): [User] +} + +"""Submission status""" +enum SubmissionStatus { + PENDING + REJECTED + PARTIALLY_ACCEPTED + ACCEPTED +} + +"""Submission sort enums""" +enum SubmissionSort { + ID + ID_DESC +} + +"""Media submission""" +type MediaSubmission { + """The id of the submission""" + id: Int! + + """User submitter of the submission""" + submitter: User + + """Data Mod assigned to handle the submission""" + assignee: User + + """Status of the submission""" + status: SubmissionStatus + submitterStats: Json + notes: String + source: String + changes: [String] + + """Whether the submission is locked""" + locked: Boolean + media: Media + submission: Media + characters: [MediaSubmissionComparison] + staff: [MediaSubmissionComparison] + studios: [MediaSubmissionComparison] + relations: [MediaEdge] + externalLinks: [MediaSubmissionComparison] + createdAt: Int +} + +"""Media submission with comparison to current data""" +type MediaSubmissionComparison { + submission: MediaSubmissionEdge + character: MediaCharacter + staff: StaffEdge + studio: StudioEdge + externalLink: MediaExternalLink +} + +type MediaSubmissionEdge { + """The id of the direct submission""" + id: Int + characterRole: CharacterRole + staffRole: String + roleNotes: String + dubGroup: String + characterName: String + isMain: Boolean + character: Character + characterSubmission: Character + voiceActor: Staff + voiceActorSubmission: Staff + staff: Staff + staffSubmission: Staff + studio: Studio + externalLink: MediaExternalLink + media: Media +} + +"""Internal - Media characters separated""" +type MediaCharacter { + """The id of the connection""" + id: Int + + """The characters role in the media""" + role: CharacterRole + roleNotes: String + dubGroup: String + + """Media specific character name""" + characterName: String + + """The characters in the media voiced by the parent actor""" + character: Character + + """The voice actor of the character""" + voiceActor: Staff +} + +"""A submission for a character that features in an anime or manga""" +type CharacterSubmission { + """The id of the submission""" + id: Int! + + """Character that the submission is referencing""" + character: Character + + """The character submission changes""" + submission: Character + + """Submitter for the submission""" + submitter: User + + """Data Mod assigned to handle the submission""" + assignee: User + + """Status of the submission""" + status: SubmissionStatus + + """Inner details of submission status""" + notes: String + source: String + + """Whether the submission is locked""" + locked: Boolean + createdAt: Int +} + +"""A submission for a staff that features in an anime or manga""" +type StaffSubmission { + """The id of the submission""" + id: Int! + + """Staff that the submission is referencing""" + staff: Staff + + """The staff submission changes""" + submission: Staff + + """Submitter for the submission""" + submitter: User + + """Data Mod assigned to handle the submission""" + assignee: User + + """Status of the submission""" + status: SubmissionStatus + + """Inner details of submission status""" + notes: String + source: String + + """Whether the submission is locked""" + locked: Boolean + createdAt: Int +} + +"""Feed of mod edit activity""" +type RevisionHistory { + """The id of the media""" + id: Int! + + """The action taken on the objects""" + action: RevisionHistoryAction + + """A JSON object of the fields that changed""" + changes: Json + + """The user who made the edit to the object""" + user: User + + """The media the mod feed entry references""" + media: Media + + """The character the mod feed entry references""" + character: Character + + """The staff member the mod feed entry references""" + staff: Staff + + """The studio the mod feed entry references""" + studio: Studio + + """The external link source the mod feed entry references""" + externalLink: MediaExternalLink + + """When the mod feed entry was created""" + createdAt: Int +} + +"""Revision history actions""" +enum RevisionHistoryAction { + CREATE + EDIT +} + +type Report { + id: Int! + reporter: User + reported: User + reason: String + + """When the entry data was created""" + createdAt: Int + cleared: Boolean +} + +type ModAction { + """The id of the action""" + id: Int! + user: User + mod: User + type: ModActionType + objectId: Int + objectType: String + data: String + createdAt: Int! +} + +enum ModActionType { + NOTE + BAN + DELETE + EDIT + EXPIRE + REPORT + RESET + ANON +} + +"""The official titles of the media in various languages""" +input MediaTitleInput { + """The romanization of the native language title""" + romaji: String + + """The official english title""" + english: String + + """Official title in it's native language""" + native: String +} + +input AiringScheduleInput { + airingAt: Int + episode: Int + timeUntilAiring: Int +} + +"""An external link to another site related to the media""" +input MediaExternalLinkInput { + """The id of the external link""" + id: Int! + + """The url of the external link""" + url: String! + + """The site location of the external link""" + site: String! +} + +"""The names of the character""" +input CharacterNameInput { + """The character's given name""" + first: String + + """The character's middle name""" + middle: String + + """The character's surname""" + last: String + + """The character's full name in their native language""" + native: String + + """Other names the character might be referred by""" + alternative: [String] + + """Other names the character might be referred to as but are spoilers""" + alternativeSpoiler: [String] +} + +type CharacterSubmissionConnection { + edges: [CharacterSubmissionEdge] + nodes: [CharacterSubmission] + + """The pagination information""" + pageInfo: PageInfo +} + +"""CharacterSubmission connection edge""" +type CharacterSubmissionEdge { + node: CharacterSubmission + + """The characters role in the media""" + role: CharacterRole + + """The voice actors of the character""" + voiceActors: [Staff] + + """The submitted voice actors of the character""" + submittedVoiceActors: [StaffSubmission] +} + +"""The names of the staff member""" +input StaffNameInput { + """The person's given name""" + first: String + + """The person's middle name""" + middle: String + + """The person's surname""" + last: String + + """The person's full name in their native language""" + native: String + + """Other names the character might be referred by""" + alternative: [String] +} + +"""User data for moderators""" +type UserModData { + alts: [User] + bans: Json + ip: Json + counts: Json + privacy: Int + email: String +} \ No newline at end of file diff --git a/Projects/Platform/AnimeDB/CoreData/DataStack.swift b/Projects/Platform/AnimeDB/CoreData/DataStack.swift new file mode 100644 index 00000000..e801768d --- /dev/null +++ b/Projects/Platform/AnimeDB/CoreData/DataStack.swift @@ -0,0 +1,39 @@ +import Foundation +import SwiftData +import Dependencies +import SwiftDataHelpers + +enum MediaDataStack { + static let modelContainer: ModelContainer = { + do { + let schema = Schema([ + SDDiscoverMedia.self, + SDMediaWatchlist.self, + SDMediaSeenlist.self, + SDMediaList.self + ]) + return try ModelContainer( + for: schema, + configurations: ModelConfiguration("Anime") + ) + } catch { + fatalError("Could not create model container: \(error)") + } + }() +} + +extension MediaDataStack: DependencyKey { + static let liveValue: Store = ContextStore(modelContainer: MediaDataStack.modelContainer) +} + +extension DependencyValues { + var animeStore: Store { + get { + self[MediaDataStack.self] + } + set { + self[MediaDataStack.self] = newValue + } + } +} + diff --git a/Projects/Platform/AnimeDB/CoreData/Entities/SDDiscoverMedia.swift b/Projects/Platform/AnimeDB/CoreData/Entities/SDDiscoverMedia.swift new file mode 100644 index 00000000..f9bcb0e8 --- /dev/null +++ b/Projects/Platform/AnimeDB/CoreData/Entities/SDDiscoverMedia.swift @@ -0,0 +1,65 @@ +import AnimeDomain +import Foundation +import SwiftData + +@Model +final class SDDiscoverMedia: Hashable { + var mediaID: Int + var startDate: Date? + var endDate: Date? + var coverImageURL: URL? + var title: String + var mediaDescription: String + var averageScore: Int + + @Relationship(inverse: \SDMediaWatchlist.media) + var watchlist: SDMediaWatchlist? + + var seenlist: SDMediaSeenlist? + + init( + mediaID: Int, + startDate: Date?, + endDate: Date?, + coverImageURL: URL?, + title: String, + mediaDescription: String, + averageScore: Int + ) { + self.mediaID = mediaID + self.startDate = startDate + self.endDate = endDate + self.coverImageURL = coverImageURL + self.title = title + self.mediaDescription = mediaDescription + self.averageScore = averageScore + } +} + +extension DiscoverMedia { + var toSDMedia: SDDiscoverMedia { + SDDiscoverMedia( + mediaID: id, + startDate: startDate, + endDate: endDate, + coverImageURL: coverImageURL, + title: title, + mediaDescription: description, + averageScore: averageScore + ) + } +} + +extension SDDiscoverMedia { + var toDomain: DiscoverMedia { + DiscoverMedia( + id: mediaID, + startDate: startDate, + endDate: endDate, + coverImageURL: coverImageURL, + title: title, + description: mediaDescription, + averageScore: averageScore + ) + } +} diff --git a/Projects/Platform/AnimeDB/CoreData/Entities/SDMediaLists.swift b/Projects/Platform/AnimeDB/CoreData/Entities/SDMediaLists.swift new file mode 100644 index 00000000..cd9f8736 --- /dev/null +++ b/Projects/Platform/AnimeDB/CoreData/Entities/SDMediaLists.swift @@ -0,0 +1,61 @@ +import Foundation +import SwiftData +import AnimeDomain + +@Model +final class SDMediaWatchlist { + @Relationship + var media: [SDDiscoverMedia] + + init(media: [SDDiscoverMedia]) { + self.media = media + } +} + +@Model +final class SDMediaSeenlist { + @Relationship + var media: [SDDiscoverMedia] + + init(media: [SDDiscoverMedia]) { + self.media = media + } +} + +@Model +final class SDMediaList { + var listID: UUID + var name: String + var imagePath: String? + + @Relationship + var media: [SDDiscoverMedia] + + init( + listID: UUID = .init(), + name: String, + imagePath: String?, + media: [SDDiscoverMedia] + ) { + self.listID = listID + self.name = name + self.imagePath = imagePath + self.media = media + } +} + +extension SDMediaWatchlist { + var toDomain: MediaWatchlist { + MediaWatchlist( + media: media.map(\.toDomain) + ) + } +} + +extension SDMediaSeenlist { + var toDomain: MediaSeenlist { + MediaSeenlist( + media: media.map(\.toDomain) + ) + } +} diff --git a/Projects/Platform/AnimeDB/MediaSeenlistRepository.swift b/Projects/Platform/AnimeDB/MediaSeenlistRepository.swift new file mode 100644 index 00000000..c20587ac --- /dev/null +++ b/Projects/Platform/AnimeDB/MediaSeenlistRepository.swift @@ -0,0 +1,58 @@ +import AnimeDomain +import Dependencies +import Foundation +import SwiftData +import SwiftDataHelpers + +public final class SDMediaSeenlistRepository: MediaSeenlistUseCase { + @Dependency(\.animeStore) + private var store: Store + private var seenlist: SDMediaSeenlist? + + public init() {} + + public func contains(media: DiscoverMedia) -> Bool { + let seenlist = try? getSDSeenlist() + return seenlist?.media.contains(where: { $0.mediaID == media.id }) ?? false + } + + public func add(media: DiscoverMedia) throws -> AnimeDomain.MediaSeenlist { + let seenlist = try getSDSeenlist() + let mediID = media.id + let predicate = #Predicate { $0.mediaID == mediID } + let fetch = FetchDescriptor(predicate: predicate) + if let sdMedia = try store.fetch(fetch).first { + seenlist.media.append(sdMedia) + } else { + let sdMedia = media.toSDMedia + seenlist.media.append(sdMedia) + } + try store.save() + return seenlist.toDomain + } + + public func remove(media: DiscoverMedia) throws -> AnimeDomain.MediaSeenlist { + let seenlist = try getSDSeenlist() + seenlist.media.removeAll(where: { $0.mediaID == media.id }) + try store.save() + return seenlist.toDomain + } + + public func getSeenList() throws -> MediaSeenlist { + try getSDSeenlist().toDomain + } + + func getSDSeenlist() throws -> SDMediaSeenlist { + if let seenlist = self.seenlist { + return seenlist + } + if let seenlist = try store.fetchAll(of: SDMediaSeenlist.self, sortBy: []).first { + return seenlist + } + let seenlist = SDMediaSeenlist(media: []) + self.seenlist = seenlist + store.insert(seenlist) + try store.save() + return seenlist + } +} diff --git a/Projects/Platform/AnimeDB/MediaWatchlistRepository.swift b/Projects/Platform/AnimeDB/MediaWatchlistRepository.swift new file mode 100644 index 00000000..0240e392 --- /dev/null +++ b/Projects/Platform/AnimeDB/MediaWatchlistRepository.swift @@ -0,0 +1,65 @@ +import Foundation +import AnimeDomain +import SwiftDataHelpers +import SwiftData +import Dependencies + +public final class SDMediaWatchlistRepository: MediaWatchlistUseCase { + @Dependency(\.animeStore) + private var store: Store + private var watchlist: SDMediaWatchlist? + + public init() {} + + public func contains(media: DiscoverMedia) -> Bool { + let watchlist = try? getSDWatchlist() + return watchlist?.media.contains(where: { $0.mediaID == media.id }) ?? false + } + + @discardableResult + public func add(media: DiscoverMedia) throws -> MediaWatchlist { + let watchlist = try getSDWatchlist() + if watchlist.media.contains(where: { media.id == $0.mediaID }) { + return watchlist.toDomain + } + let mediaId = media.id + let predicate = #Predicate { + $0.mediaID == mediaId + } + let fetch = FetchDescriptor(predicate: predicate) + if let sdMedia = try store.fetch(fetch).first { + watchlist.media.append(sdMedia) + } else { + let sdMedia = media.toSDMedia + watchlist.media.append(sdMedia) + } + try store.save() + return watchlist.toDomain + } + + @discardableResult + public func remove(media: DiscoverMedia) throws -> MediaWatchlist { + let watchlist = try getSDWatchlist() + watchlist.media.removeAll(where: { $0.mediaID == media.id }) + try store.save() + return watchlist.toDomain + } + + public func getWatchlist() throws -> MediaWatchlist { + try getSDWatchlist().toDomain + } + + private func getSDWatchlist() throws -> SDMediaWatchlist { + if let watchlist = self.watchlist { + return watchlist + } + if let watchlist = try store.fetchAll(of: SDMediaWatchlist.self, sortBy: []).first { + return watchlist + } + let watchlist = SDMediaWatchlist(media: []) + self.watchlist = watchlist + store.insert(watchlist) + try store.save() + return watchlist + } +} diff --git a/Projects/Platform/MoviesAPI/Geteways/DiscoverMoviesGateway.swift b/Projects/Platform/MoviesAPI/Geteways/DiscoverMoviesGateway.swift new file mode 100644 index 00000000..f80c492f --- /dev/null +++ b/Projects/Platform/MoviesAPI/Geteways/DiscoverMoviesGateway.swift @@ -0,0 +1,45 @@ +import Foundation +import HTTPClient +import MoviesDomain + +public final class DiscoverMoviesGateway: MoviesDomain.DiscoverMoviesGateway { + private let client: DataFetching + private let decoder = JSONDecoder() + private let dateDormatter = DateFormatter() + + public init(client: DataFetching) { + self.client = client + dateDormatter.dateFormat = "YYYY-MM-DD" + decoder.userInfo[.dateFormatter] = dateDormatter + } + + public func fetch(request: DiscoverMoviesRequest) async throws -> PageResult { + do { + let data = try await client.fetch(resource: request.makeResource()) + let page = try decoder.decode(PageResult.self, from: data) + return page + } catch let error as NetworkError { + if case .notConnectedToInternet = error { + throw OfflineError() + } + throw error + } catch { + throw error + } + } +} + +extension DiscoverMoviesRequest { + func makeResource() -> Resource { + switch self { + case .nowPlaying: + return Resource(path: "/movie/now_playing", query: [:]) + case .popular: + return Resource(path: "/movie/popular", query: [:]) + case .topRated: + return Resource(path: "/movie/top_rated", query: [:]) + case .upcoming: + return Resource(path: "/movie/upcoming", query: [:]) + } + } +} diff --git a/Projects/Platform/MoviesAPI/Geteways/MovieCreditsGateway.swift b/Projects/Platform/MoviesAPI/Geteways/MovieCreditsGateway.swift new file mode 100644 index 00000000..8d83a3eb --- /dev/null +++ b/Projects/Platform/MoviesAPI/Geteways/MovieCreditsGateway.swift @@ -0,0 +1,20 @@ +import Foundation +import HTTPClient +import MoviesDomain + +public final class MovieCreditsGateway: MovieCreditsUseCase { + private let client: DataFetching + private let decoder = JSONDecoder() + + public init(client: DataFetching) { + self.client = client + } + + public func fetchCast(movieID: MovieID) async throws -> MovieCast { + let resource = Resource(path: "/movie/\(movieID.rawValue)/credits") + let data = try await client.fetch(resource: resource) + let cast = try decoder.decode(MovieCast.self, from: data) + + return cast + } +} diff --git a/Projects/Platform/MoviesAPI/Geteways/MovieDetailsGateway.swift b/Projects/Platform/MoviesAPI/Geteways/MovieDetailsGateway.swift new file mode 100644 index 00000000..14dcfca0 --- /dev/null +++ b/Projects/Platform/MoviesAPI/Geteways/MovieDetailsGateway.swift @@ -0,0 +1,33 @@ +import Foundation +import HTTPClient +import MoviesDomain + +public final class MovieDetailsGateway: MovieDetailUseCaseProtocol { + private let client: DataFetching + private let decoder = JSONDecoder() + private let dateDormatter = DateFormatter() + + public init(client: DataFetching) { + self.client = client + dateDormatter.dateFormat = "YYYY-MM-DD" + decoder.dateDecodingStrategy = .formatted(dateDormatter) + decoder.keyDecodingStrategy = .convertFromSnakeCase + } + + public func fetchDetail(for movieID: MovieID) async throws -> MovieDetail { + do { + let query = ["append_to_response": "keywords"] + let resource = Resource(path: "/movie/\(movieID.rawValue)", query: query) + let data = try await client.fetch(resource: resource) + let page = try decoder.decode(MovieDetail.self, from: data) + return page + } catch let error as NetworkError { + if case .notConnectedToInternet = error { + throw OfflineError() + } + throw error + } catch { + throw error + } + } +} diff --git a/Projects/Platform/MoviesAPI/Geteways/MovieRecomendationGateway.swift b/Projects/Platform/MoviesAPI/Geteways/MovieRecomendationGateway.swift new file mode 100644 index 00000000..d886fbee --- /dev/null +++ b/Projects/Platform/MoviesAPI/Geteways/MovieRecomendationGateway.swift @@ -0,0 +1,29 @@ +import MoviesDomain +import HTTPClient +import Foundation + +public final class MovieRecomendationGateway: MovieRecomendationUseCase { + private let client: DataFetching + private let decoder = JSONDecoder() + private let dateDormatter = DateFormatter() + + public init(client: DataFetching) { + self.client = client + dateDormatter.dateFormat = "YYYY-MM-DD" + decoder.userInfo[.dateFormatter] = dateDormatter + } + + public func fetchSimilar(movieID: MovieID) async throws -> [Movie] { + let resource = Resource(path: "/movie/\(movieID.rawValue)/recommendations") + let data = try await client.fetch(resource: resource) + let result = try decoder.decode(PageResult.self, from: data) + return result.results + } + + public func fetchRecomended(movieID: MovieID) async throws -> [Movie] { + let resource = Resource(path: "/movie/\(movieID.rawValue)/similar") + let data = try await client.fetch(resource: resource) + let result = try decoder.decode(PageResult.self, from: data) + return result.results + } +} diff --git a/Projects/Platform/MoviesAPI/Geteways/PersonDetailGateway.swift b/Projects/Platform/MoviesAPI/Geteways/PersonDetailGateway.swift new file mode 100644 index 00000000..8520fd0c --- /dev/null +++ b/Projects/Platform/MoviesAPI/Geteways/PersonDetailGateway.swift @@ -0,0 +1,29 @@ +import MoviesDomain +import HTTPClient +import Foundation + +public final class PersonDetailsGateway: PersonDetailsUseCase { + private let client: DataFetching + private let decoder = JSONDecoder() + private let dateDormatter = DateFormatter() + + public init(client: DataFetching) { + self.client = client + dateDormatter.dateFormat = "YYYY-MM-DD" + decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .formatted(dateDormatter) + } + + public func fetchPersonDetails(with id: PersonID) async throws -> PersonDetails { + let resource = Resource( + path: "/person/\(id.rawValue)", + query: [ + "append_to_response": "images" + ] + ) + let data = try await client.fetch(resource: resource) + let person = try decoder.decode(PersonDetails.self, from: data) + + return person + } +} diff --git a/Projects/Platform/MoviesAPI/Geteways/SearchMoviesGateway.swift b/Projects/Platform/MoviesAPI/Geteways/SearchMoviesGateway.swift new file mode 100644 index 00000000..4a0abbd3 --- /dev/null +++ b/Projects/Platform/MoviesAPI/Geteways/SearchMoviesGateway.swift @@ -0,0 +1,23 @@ +import Foundation +import HTTPClient +import MoviesDomain + +public final class SearchMoviesGateway: MovieSearchUseCase { + private let client: DataFetching + private let decoder = JSONDecoder() + private let dateDormatter = DateFormatter() + + public init(client: DataFetching) { + self.client = client + dateDormatter.dateFormat = "YYYY-MM-DD" + decoder.userInfo[.dateFormatter] = dateDormatter + } + + public func search(query: String, page: Int) async throws -> PageResult { + let resource = Resource(path: "/search/movie", query: ["query": query, "page": "\(page)"]) + let data = try await client.fetch(resource: resource) + let page = try decoder.decode(PageResult.self, from: data) + + return page + } +} diff --git a/Projects/Platform/MoviesDB/CoreData/DataStore.swift b/Projects/Platform/MoviesDB/CoreData/DataStore.swift new file mode 100644 index 00000000..c0deef80 --- /dev/null +++ b/Projects/Platform/MoviesDB/CoreData/DataStore.swift @@ -0,0 +1,38 @@ +import Foundation +import SwiftData +import Dependencies +import SwiftDataHelpers + +enum MoviesDataStack { + static let modelContainer: ModelContainer = { + do { + let schema = Schema([ + SDMovie.self, + SDMovieWatchlist.self, + SDMovieSeenList.self, + SDMovieList.self + ]) + return try ModelContainer( + for: schema, + configurations: ModelConfiguration("Movies") + ) + } catch { + fatalError("Could not create model container: \(error)") + } + }() +} + +extension MoviesDataStack: DependencyKey { + static let liveValue: Store = ContextStore(modelContainer: MoviesDataStack.modelContainer) +} + +extension DependencyValues { + var moviesStore: Store { + get { + self[MoviesDataStack.self] + } + set { + self[MoviesDataStack.self] = newValue + } + } +} diff --git a/Projects/Platform/MoviesDB/CoreData/Entities/SDMovie.swift b/Projects/Platform/MoviesDB/CoreData/Entities/SDMovie.swift new file mode 100644 index 00000000..c6fad65b --- /dev/null +++ b/Projects/Platform/MoviesDB/CoreData/Entities/SDMovie.swift @@ -0,0 +1,86 @@ +import Foundation +import MoviesDomain +import SwiftData +import Tagged + +@Model +final class SDMovie { + @Attribute(.unique) var movieId: Int + + var adult: Bool + var backdropPath: String? + var overview: String + var popularity: Double + var posterPath: String? + var releaseDate: Date? + var title: String + var video: Bool + var voteAverage: Double + var voteCount: Int + + @Relationship(inverse: \SDMovieWatchlist.movies) + var watchlist: SDMovieWatchlist? + + @Relationship(inverse: \SDMovieSeenList.movies) + var seenlist: SDMovieSeenList? + + @Relationship(inverse: \SDMovieList.movies) + var lists: [SDMovieList] = [] + + init(adult: Bool, backdropPath: String?, movieId: Int, overview: String, popularity: Double, posterPath: String?, releaseDate: Date?, title: String, video: Bool, voteAverage: Double, voteCount: Int) { + self.adult = adult + self.backdropPath = backdropPath + self.movieId = movieId + self.overview = overview + self.popularity = popularity + self.posterPath = posterPath + self.releaseDate = releaseDate + self.title = title + self.video = video + self.voteAverage = voteAverage + self.voteCount = voteCount + } +} + +extension SDMovie { + var toDomain: Movie { + Movie( + adult: adult, + backdropPath: backdropPath, + id: .init(movieId), + overview: overview, + popularity: popularity, + posterPath: posterPath, + releaseDate: releaseDate, + title: title, + video: video, + voteAverage: voteAverage, + voteCount: voteCount + ) + } + + + static func by(movieId: Int) -> Predicate { + #Predicate { + $0.movieId == movieId + } + } +} + +extension Movie { + var toSDMovie: SDMovie { + SDMovie( + adult: adult, + backdropPath: backdropPath, + movieId: id.rawValue, + overview: overview, + popularity: popularity, + posterPath: posterPath, + releaseDate: releaseDate, + title: title, + video: video, + voteAverage: voteAverage, + voteCount: voteCount + ) + } +} diff --git a/Projects/Platform/MoviesDB/CoreData/Entities/SDMovieList.swift b/Projects/Platform/MoviesDB/CoreData/Entities/SDMovieList.swift new file mode 100644 index 00000000..ff42d9ae --- /dev/null +++ b/Projects/Platform/MoviesDB/CoreData/Entities/SDMovieList.swift @@ -0,0 +1,79 @@ +import Foundation +import SwiftData +import MoviesDomain + +@Model +final class SDMovieWatchlist { + @Attribute(.unique) + private var listID = 1 + + @Relationship + var movies: [SDMovie] + + init(movies: [SDMovie]) { + self.movies = movies + } + + var toDomain: MovieWatchlist { + MovieWatchlist(movies: movies.map(\.toDomain)) + } +} + +@Model +final class SDMovieSeenList { + @Attribute(.unique) + private var listID = 1 + + @Relationship + var movies: [SDMovie] + + init(movies: [SDMovie]) { + self.movies = movies + } + + var toDomain: MovieSeenList { + MovieSeenList(movies: movies.map(\.toDomain)) + } +} + +@Model +final class SDMovieList { + @Attribute(.unique) + private(set) var listID: String + var name: String + var imagePath: String? + + @Relationship + var movies: [SDMovie] + var createdAt: Date + + init( + listID: String, + name: String, + imagePath: String?, + movies: [SDMovie] + ) { + self.listID = listID + self.name = name + self.imagePath = imagePath + self.movies = movies + self.createdAt = Date() + } + + var toDomain: MovieList { + MovieList( + id: UUID(uuidString: listID) ?? UUID(), + name: name, + imagePath: imagePath, + movies: movies.map(\.toDomain) + ) + } +} + +extension SDMovieList { + static func by(listID: String) -> Predicate { + #Predicate { + $0.listID == listID + } + } +} diff --git a/Projects/Platform/MoviesDB/DicoverMoviesRepository.swift b/Projects/Platform/MoviesDB/DicoverMoviesRepository.swift new file mode 100644 index 00000000..8fb7b48f --- /dev/null +++ b/Projects/Platform/MoviesDB/DicoverMoviesRepository.swift @@ -0,0 +1,70 @@ +import FileCache +import Foundation +import MoviesDomain +import SwiftData +import Dependencies +import SwiftDataHelpers + +public final class DicoverMoviesRepository: MoviesDomain.DicoverMoviesRepository { + private let fileCache = FileCache(name: "DicoverMoviesRepository") + private let encoder = JSONEncoder() + private let decoder = JSONDecoder() + + public init() {} + + public func movies( + for reuqest: MoviesDomain.DiscoverMoviesRequest + ) throws -> [Movie] { + let data = try fileCache.loadFile(path: reuqest.repoPath) + let movies = try decoder.decode([Movie].self, from: data) + return movies + } + + public func save(movies: [Movie], for request: DiscoverMoviesRequest) throws { + try fileCache.persist(item: movies, encoder: encoder, path: request.repoPath) + } +} + +public final class SDDicoverMoviesRepository: MoviesDomain.DicoverMoviesRepository { + @Dependency(\.moviesStore) + private var store: Store + + public init() {} + + public func movies(for reuqest: MoviesDomain.DiscoverMoviesRequest) throws -> [MoviesDomain.Movie] { + let movies = try store.fetchAll(of: SDMovie.self, sortBy: []) + + return movies.map(\.toDomain) + } + + public func save(movies: [Movie], for request: DiscoverMoviesRequest) throws { + let movieIDs = movies.map(\.id.rawValue) + let discriptor = FetchDescriptor( + predicate: #Predicate { item in + movieIDs.contains(item.movieId) + } + ) + let alreadyStoredMovies = Set(try store.fetch(discriptor).map(\.movieId)) + for movie in movies { + if alreadyStoredMovies.contains(movie.id.rawValue) == false { + store.insert(movie.toSDMovie) + } + } + try store.save() + } +} + +private extension DiscoverMoviesRequest { + var repoPath: String { + switch self { + case .nowPlaying: + return "nowPlaying" + case .popular: + return "popular" + case .topRated: + return "topRated" + case .upcoming: + return "upcoming" + } + } +} diff --git a/Projects/Platform/MoviesDB/MovieListRepository.swift b/Projects/Platform/MoviesDB/MovieListRepository.swift new file mode 100644 index 00000000..1f069bc4 --- /dev/null +++ b/Projects/Platform/MoviesDB/MovieListRepository.swift @@ -0,0 +1,130 @@ +import Dependencies +import FileCache +import Foundation +import MoviesDomain +import SwiftData +import SwiftDataHelpers + +public final class MovieListRepository: MovieListUseCase { + private let fileCache = FileCache(name: "MovieListRepository") + private let customListsPath = "custom_lists.json" + private let moviesDateFormatter = DateFormatter() + private let dateFormatter = DateFormatter() + private let decoder = JSONDecoder() + private let encoder = JSONEncoder() + + public init() { + moviesDateFormatter.dateFormat = "YYYY-MM-DD" + decoder.userInfo[.dateFormatter] = dateFormatter + encoder.userInfo[.dateFormatter] = dateFormatter + } + + public func getCustomLists() throws -> [MovieList] { + if fileCache.exists(atPath: customListsPath) == false { + let emptyList: [MovieList] = [] + try fileCache.persist(data: encoder.encode(emptyList), path: customListsPath) + return emptyList + } + let data = try fileCache.loadFile(path: customListsPath) + let customLists = try decoder.decode([MovieList].self, from: data) + return customLists + } + + public func add(movie: Movie, to list: MovieList) throws -> MovieList { + var customLists = try getCustomLists() + guard let index = customLists.firstIndex(where: { $0.id == list.id }) else { + throw MovieListError.listNotFound + } + customLists[index].movies.append(movie) + try save(lists: customLists) + return customLists[index] + } + + public func remove(movie: Movie, from list: MovieList) throws -> MovieList { + var customLists = try getCustomLists() + guard let index = customLists.firstIndex(where: { $0.id == list.id }) else { + throw MovieListError.listNotFound + } + customLists[index].movies.removeAll(where: { $0.id == movie.id }) + try save(lists: customLists) + return customLists[index] + } + + @discardableResult + public func create(name: String, imagePath: String?) throws -> MovieList { + let newList = MovieList(name: name, imagePath: imagePath, movies: []) + guard fileCache.exists(atPath: customListsPath) else { + try save(lists: [newList]) + return newList + } + let data = try fileCache.loadFile(path: customListsPath) + var customLists = try decoder.decode([MovieList].self, from: data) + customLists.append(newList) + + try save(lists: customLists) + + return newList + } + + public func isMovieInMovieList(_ movie: Movie) -> Bool { + let lists = (try? getCustomLists()) ?? [] + + for list in lists { + if list.movies.contains(where: { $0.id == movie.id }) { + return true + } + } + return false + } + + private func save(lists: [MovieList]) throws { + let data = try encoder.encode(lists) + try fileCache.persist(data: data, path: customListsPath) + } +} + +public final class SDMovieListRepository: MovieListUseCase { + @Dependency(\.moviesStore) + private var store: Store + + public init() {} + + public func getCustomLists() throws -> [MovieList] { + try store.fetchAll(of: SDMovieList.self, sortBy: [SortDescriptor(\.createdAt, order: .reverse)]) + .map(\.toDomain) + } + + public func add(movie: Movie, to list: MovieList) throws -> MovieList { + guard let sdList = try store.fetchFirst(.init(predicate: SDMovieList.by(listID: list.id.uuidString))) else { + throw Error.listNotFound + } + let sdMovie = try store.fetchFirst(.init(predicate: SDMovie.by(movieId: movie.id.rawValue))) ?? movie.toSDMovie + sdList.movies.append(sdMovie) + try store.save() + return sdList.toDomain + } + + public func remove(movie: MoviesDomain.Movie, from list: MovieList) throws -> MovieList { + return list + } + + public func create(name: String, imagePath: String?) throws -> MovieList { + let id = UUID() + let sdList = SDMovieList(listID: id.uuidString, name: name, imagePath: imagePath, movies: []) + store.insert(sdList) + try store.save() + return MovieList(id: id, name: name, imagePath: imagePath, movies: []) + } + + public func isMovieInMovieList(_ movie: Movie) -> Bool { + guard let sdMovie = try? store.fetchFirst(.init(predicate: SDMovie.by(movieId: movie.id.rawValue))) else { + return false + } + + return sdMovie.lists.isEmpty + } + + enum Error: Swift.Error { + case listNotFound + } +} diff --git a/Projects/Platform/MoviesDB/MovieSeenlistRepository.swift b/Projects/Platform/MoviesDB/MovieSeenlistRepository.swift new file mode 100644 index 00000000..a0fa2588 --- /dev/null +++ b/Projects/Platform/MoviesDB/MovieSeenlistRepository.swift @@ -0,0 +1,109 @@ +import FileCache +import Foundation +import MoviesDomain +import Dependencies +import SwiftData +import SwiftDataHelpers + +public final class MovieSeenlistRepository: MovieSeenlistUseCase { + private let fileCache = FileCache(name: "MovieSeenlistRepository") + private let watchlistPath = "watchlist.json" + private let seenListPath = "seenlist.json" + private let customListsPath = "custom_list.json" + private let moviesDateFormatter = DateFormatter() + private let dateFormatter = DateFormatter() + private let decoder = JSONDecoder() + private let encoder = JSONEncoder() + + public init () { + moviesDateFormatter.dateFormat = "YYYY-MM-DD" + decoder.userInfo[.dateFormatter] = dateFormatter + encoder.userInfo[.dateFormatter] = dateFormatter + } + + @discardableResult + public func add(movie: Movie) throws -> MovieSeenList { + var seenList = try getSeenList() + if seenList.movies.contains(where: { movie.id == $0.id } ) { + return seenList + } + seenList.movies.append(movie) + try fileCache.persist(data: encoder.encode(seenList), path: seenListPath) + return seenList + } + + @discardableResult + public func remove(movie: Movie) throws -> MovieSeenList { + var seenList = try getSeenList() + seenList.movies.removeAll(where: { $0.id == movie.id }) + try fileCache.persist(data: encoder.encode(seenList), path: seenListPath) + return seenList + } + + public func getSeenList() throws -> MovieSeenList { + if fileCache.exists(atPath: seenListPath) == false { + let movieSeenList = MovieSeenList(movies: []) + try fileCache.persist(data: encoder.encode(movieSeenList), path: seenListPath) + return movieSeenList + } + let data = try fileCache.loadFile(path: seenListPath) + let movieSeenList = try decoder.decode(MovieSeenList.self, from: data) + return movieSeenList + } + + public func contains(movie: Movie) -> Bool { + let seenList = (try? getSeenList()) ?? MovieSeenList(movies: []) + return seenList.movies.contains(where: { $0.id == movie.id }) + } +} + +public final class SDMovieSeenlistRepository: MovieSeenlistUseCase { + @Dependency(\.moviesStore) + private var store: Store + private var seenlist: SDMovieSeenList? + + public init() {} + + public func contains(movie: Movie) -> Bool { + let seenlist = try? getSDSeenlist() + return seenlist?.movies.contains(where: { $0.movieId == movie.id.rawValue }) ?? false + } + + public func add(movie: Movie) throws -> MovieSeenList { + let seenlist = try getSDSeenlist() + let fetch = FetchDescriptor(predicate: #Predicate { $0.movieId == movie.id.rawValue }) + if let sdMovie = try store.fetch(fetch).first { + seenlist.movies.append(sdMovie) + } else { + let sdMovie = movie.toSDMovie + seenlist.movies.append(sdMovie) + } + try store.save() + return seenlist.toDomain + } + + public func remove(movie: Movie) throws -> MovieSeenList { + let seenlist = try getSDSeenlist() + seenlist.movies.removeAll(where: { $0.movieId == movie.id.rawValue }) + try store.save() + return seenlist.toDomain + } + + public func getSeenList() throws -> MovieSeenList { + try getSDSeenlist().toDomain + } + + func getSDSeenlist() throws -> SDMovieSeenList { + if let seenlist = self.seenlist { + return seenlist + } + if let seenlist = try store.fetchAll(of: SDMovieSeenList.self, sortBy: []).first { + return seenlist + } + let seenlist = SDMovieSeenList(movies: []) + self.seenlist = seenlist + store.insert(seenlist) + try store.save() + return seenlist + } +} diff --git a/Projects/Platform/MoviesDB/MovieWatchlistRepository.swift b/Projects/Platform/MoviesDB/MovieWatchlistRepository.swift new file mode 100644 index 00000000..c470742a --- /dev/null +++ b/Projects/Platform/MoviesDB/MovieWatchlistRepository.swift @@ -0,0 +1,112 @@ +import Dependencies +import FileCache +import Foundation +import MoviesDomain +import SwiftData +import SwiftDataHelpers + +@MainActor +public final class MovieWatchlistRepository: MovieWatchlistUseCase { + private let fileCache = FileCache(name: "MovieWatchlistRepository") + private let watchlistPath = "watchlist.json" + private let moviesDateFormatter = DateFormatter() + private let dateFormatter = DateFormatter() + private let decoder = JSONDecoder() + private let encoder = JSONEncoder() + + public init() { + moviesDateFormatter.dateFormat = "YYYY-MM-DD" + decoder.userInfo[.dateFormatter] = dateFormatter + encoder.userInfo[.dateFormatter] = dateFormatter + } + + @discardableResult + public func add(movie: Movie) throws -> MovieWatchlist { + var watchlist = try getWatchlist() + if watchlist.movies.contains(movie) { + return watchlist + } + watchlist.movies.append(movie) + try fileCache.persist(data: encoder.encode(watchlist), path: watchlistPath) + return watchlist + } + + @discardableResult + public func remove(movie: Movie) throws -> MovieWatchlist { + var watchlist = try getWatchlist() + watchlist.movies.removeAll(where: { $0.id == movie.id }) + try fileCache.persist(data: encoder.encode(watchlist), path: watchlistPath) + return watchlist + } + + public func getWatchlist() throws -> MovieWatchlist { + if fileCache.exists(atPath: watchlistPath) == false { + let movieWatchlist = MovieWatchlist(movies: []) + try fileCache.persist(data: encoder.encode(movieWatchlist), path: watchlistPath) + return movieWatchlist + } + let data = try fileCache.loadFile(path: watchlistPath) + let movieWatchlist = try decoder.decode(MovieWatchlist.self, from: data) + return movieWatchlist + } + + public func contains(movie: Movie) -> Bool { + let watchlist = (try? getWatchlist()) ?? MovieWatchlist(movies: []) + return watchlist.movies.contains(where: { $0.id == movie.id }) + } +} + +@MainActor +public final class SDMovieWatchlistRepository: MovieWatchlistUseCase { + @Dependency(\.moviesStore) + private var store: Store + private var watchlist: SDMovieWatchlist? + + public init() {} + + public func contains(movie: Movie) -> Bool { + let watchlist = try? getSDWatchlist() + return watchlist?.movies.contains(where: { $0.movieId == movie.id.rawValue }) ?? false + } + + public func add(movie: Movie) throws -> MovieWatchlist { + let watchlist = try getSDWatchlist() + if watchlist.movies.contains(where: { movie.id.rawValue == $0.movieId }) { + return watchlist.toDomain + } + let fetch = FetchDescriptor(predicate: #Predicate { $0.movieId == movie.id.rawValue }) + if let sdMovie = try store.fetch(fetch).first { + watchlist.movies.append(sdMovie) + } else { + let sdMovie = movie.toSDMovie + watchlist.movies.append(sdMovie) + } + try store.save() + return watchlist.toDomain + } + + public func remove(movie: Movie) throws -> MovieWatchlist { + let watchlist = try getSDWatchlist() + watchlist.movies.removeAll(where: { $0.movieId == movie.id.rawValue }) + try store.save() + return watchlist.toDomain + } + + public func getWatchlist() throws -> MovieWatchlist { + try getSDWatchlist().toDomain + } + + func getSDWatchlist() throws -> SDMovieWatchlist { + if let watchlist = self.watchlist { + return watchlist + } + if let watchlist = try store.fetchAll(of: SDMovieWatchlist.self, sortBy: []).first { + return watchlist + } + let watchlist = SDMovieWatchlist(movies: []) + self.watchlist = watchlist + store.insert(watchlist) + try store.save() + return watchlist + } +} diff --git a/Projects/Platform/Project.swift b/Projects/Platform/Project.swift new file mode 100644 index 00000000..dd63c20b --- /dev/null +++ b/Projects/Platform/Project.swift @@ -0,0 +1,4 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let project = ProjectName.Platform.project diff --git a/Projects/UI/Project.swift b/Projects/UI/Project.swift new file mode 100644 index 00000000..bac9fa9a --- /dev/null +++ b/Projects/UI/Project.swift @@ -0,0 +1,4 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let project = ProjectName.UI.project diff --git a/Projects/UI/UI/Components/BackdropImageView.swift b/Projects/UI/UI/Components/BackdropImageView.swift new file mode 100644 index 00000000..c7032fb6 --- /dev/null +++ b/Projects/UI/UI/Components/BackdropImageView.swift @@ -0,0 +1,36 @@ +import SwiftUI + +public struct BackdropImageView: View { + let posterURL: URL? + let height: CGFloat? + + public init(posterURL: URL?, height: CGFloat? = nil) { + self.posterURL = posterURL + self.height = height + } + + public var body: some View { + Group { + if let posterURL = posterURL { + URLImage(url: posterURL) { content in + switch content { + case .success(let image): + image.resizable() + .renderingMode(.original) + .transition(.opacity) + case .failure, .empty: + Rectangle() + .foregroundColor(.gray) + @unknown default: + Rectangle() + .foregroundColor(.gray) + } + } + } else { + Rectangle() + .foregroundColor(.gray) + } + } + .frame(height: height) + } +} diff --git a/Projects/UI/UI/Components/BorderedButton.swift b/Projects/UI/UI/Components/BorderedButton.swift new file mode 100644 index 00000000..3c0255ed --- /dev/null +++ b/Projects/UI/UI/Components/BorderedButton.swift @@ -0,0 +1,64 @@ +import SwiftUI + +public struct BorderedButton: View { + public let isSelected: Bool + public let text: String + public let systemImageName: String + public let color: Color + public let action: () -> Void + + @Environment(\.isEnabled) var isEnabled + + public init( + isSelected: Bool, + text: String, + systemImageName: String, + color: Color, + action: @escaping () -> Void + ) { + self.isSelected = isSelected + self.text = text + self.systemImageName = systemImageName + self.color = color + self.action = action + } + + public var body: some View { + Button(action: { + self.action() + }, label: { + HStack(alignment: .center, spacing: 4) { + Image(systemName: systemImageName) + .foregroundColor(isSelected ? .white : color) + Text(text) + .foregroundColor(isSelected ? .white : color) + } + .font(.caption) + }) + .buttonStyle( + BorderedButtonStyle(color: color, isSelected: isSelected) + ) + } +} + +struct BorderedButtonStyle: ButtonStyle { + var color: Color + var isSelected: Bool + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .padding(4) + .foregroundStyle(isSelected ? .red : color) + .background( + RoundedRectangle(cornerRadius: 8) + .stroke(color, lineWidth: isSelected ? 0 : 2) + .background(isSelected ? color : .clear) + .cornerRadius(8) + ) + } +} + +#Preview { + BorderedButton(isSelected: false, text: "Seenlist", systemImageName: "eye", color: .green, action: {}) + BorderedButton(isSelected: true, text: "Seenlist", systemImageName: "eye", color: .green, action: {}) +} diff --git a/Projects/UI/UI/Components/CheckToggleStyle.swift b/Projects/UI/UI/Components/CheckToggleStyle.swift new file mode 100644 index 00000000..17207f4e --- /dev/null +++ b/Projects/UI/UI/Components/CheckToggleStyle.swift @@ -0,0 +1,23 @@ +import SwiftUI + +extension View { + public func withCheckmark(isOn: Binding) -> some View { + self.modifier(WithCheckmark(isOn: isOn)) + } +} + +struct WithCheckmark: ViewModifier { + let isOn: Binding + + func body(content: Content) -> some View { + HStack { + content + Toggle(isOn: isOn) { + Image( + systemName: isOn.wrappedValue ? "checkmark.circle.fill" : "circle") + } + .toggleStyle(.button) + .clipShape(Circle()) + } + } +} diff --git a/Projects/UI/UI/Components/MediaCrossLineItemsRow.swift b/Projects/UI/UI/Components/MediaCrossLineItemsRow.swift new file mode 100644 index 00000000..a1085bdf --- /dev/null +++ b/Projects/UI/UI/Components/MediaCrossLineItemsRow.swift @@ -0,0 +1,65 @@ +import SwiftUI + +public struct MediaCrossLineItemsRow: View { + let title: String + let posterSize: PosterStyle.Size + let items: [Item] + + public init( + title: String, + posterSize: PosterStyle.Size, + items: [Item] + ) { + self.title = title + self.items = items + self.posterSize = posterSize + } + + public var body: some View { + VStack(alignment: .leading) { + Text(title) + .foregroundColor(.primary) + .font(.headline) + + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack { + ForEach(items, id: \.id) { (item) in + MediaImageCell( + imageURL: item.imageURL, + title: item.title, + subtitle: item.subtitle, + posterSize: posterSize + ) + .onTapGesture { + item.didTap() + } + } + } + } + } + .padding(.vertical) + .contentShape(Rectangle()) + } + + public struct Item { + public let id: String + public let imageURL: URL? + public let title: String? + public let subtitle: String? + public let didTap: () -> Void + + public init( + id: String, + imageURL: URL?, + title: String? = nil, + subtitle: String? = nil, + didTap: @escaping () -> Void + ) { + self.id = id + self.imageURL = imageURL + self.title = title + self.subtitle = subtitle + self.didTap = didTap + } + } +} diff --git a/Projects/UI/UI/Components/MediaDetailsButtons.swift b/Projects/UI/UI/Components/MediaDetailsButtons.swift new file mode 100644 index 00000000..9563094e --- /dev/null +++ b/Projects/UI/UI/Components/MediaDetailsButtons.swift @@ -0,0 +1,55 @@ +import SwiftUI + +public struct MediaButtonsRow: View { + var isWatchlistSelected: Bool + var isSeenlistSelected: Bool + var isListSelected: Bool + + var didTapWatchlist: () -> Void + var didTapSeenlist: () -> Void + var didTapList: () -> Void + + public init( + isWatchlistSelected: Bool, + isSeenlistSelected: Bool, + isListSelected: Bool, + didTapWatchlist: @escaping () -> Void, + didTapSeenlist: @escaping () -> Void, + didTapList: @escaping () -> Void + ) { + self.isWatchlistSelected = isWatchlistSelected + self.isSeenlistSelected = isSeenlistSelected + self.isListSelected = isListSelected + self.didTapWatchlist = didTapWatchlist + self.didTapSeenlist = didTapSeenlist + self.didTapList = didTapList + } + + public var body: some View { + HStack { + BorderedButton( + isSelected: isWatchlistSelected, + text: isWatchlistSelected ? "In Watchlist" : "Watchlist", + systemImageName: "heart", + color: .pink, + action: didTapWatchlist + ) + BorderedButton( + isSelected: isSeenlistSelected, + text: isSeenlistSelected ? "Seen" : "Seenlist", + systemImageName: "eye", + color: .orange, + action: didTapSeenlist + ) + BorderedButton( + isSelected: isListSelected, + text: "List", + systemImageName: "pin", + color: .indigo, + action: didTapList + ) + } + .padding([.top, .bottom], 4) + .font(.callout) + } +} diff --git a/Projects/UI/UI/Components/MediaImageCell.swift b/Projects/UI/UI/Components/MediaImageCell.swift new file mode 100644 index 00000000..2f454e79 --- /dev/null +++ b/Projects/UI/UI/Components/MediaImageCell.swift @@ -0,0 +1,38 @@ +import SwiftUI + +public struct MediaImageCell: View { + public let imageURL: URL? + public let title: String? + public let subtitle: String? + public let posterSize: PosterStyle.Size + + public init(imageURL: URL?, title: String?, subtitle: String?, posterSize: PosterStyle.Size = .small) { + self.imageURL = imageURL + self.title = title + self.subtitle = subtitle + self.posterSize = posterSize + } + + public var body: some View { + VStack(alignment: .center) { + PosterImageView( + posterSize: posterSize, + posterURL: imageURL + ) + if let title { + Text(title) + .font(.footnote) + .foregroundColor(.primary) + .lineLimit(1) + } + if let subtitle { + Text(subtitle) + .font(.footnote) + .foregroundColor(.secondary) + .lineLimit(1) + } + } + .frame(width: 100) + .contentShape(Rectangle()) + } +} diff --git a/Projects/UI/UI/Components/MediaOverviewView.swift b/Projects/UI/UI/Components/MediaOverviewView.swift new file mode 100644 index 00000000..0190f641 --- /dev/null +++ b/Projects/UI/UI/Components/MediaOverviewView.swift @@ -0,0 +1,33 @@ +import SwiftUI + +public struct MediaOverviewView: View { + let overview: String + + @State + var isExpanded: Bool = false + + public init(overview: String) { + self.overview = overview + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Overview:") + .foregroundColor(.primary) + .font(.headline) + .lineLimit(1) + + Text(overview) + .font(.subheadline) + .foregroundColor(.secondary) + .lineLimit(isExpanded ? nil : 5) + + Button(action: { + isExpanded.toggle() + }) { + Text(isExpanded ? "Less.." : "More..") + .font(.caption) + } + } + } +} diff --git a/Projects/UI/UI/Components/MediaRow.swift b/Projects/UI/UI/Components/MediaRow.swift new file mode 100644 index 00000000..6823a3e2 --- /dev/null +++ b/Projects/UI/UI/Components/MediaRow.swift @@ -0,0 +1,64 @@ +import SwiftUI + +public struct MediaRow: View { + let title: String + let posterURL: URL? + let score: Int + let releaseDate: Date? + let overview: String + + public init(title: String, posterURL: URL?, score: Int, releaseDate: Date?, overview: String) { + self.title = title + self.posterURL = posterURL + self.score = score + self.releaseDate = releaseDate + self.overview = overview + } + + public var body: some View { + HStack { + PosterImageView( + posterSize: .medium, + posterURL: posterURL + ) + .fixedSize() + VStack(alignment: .leading, spacing: 8) { + Text(title) + .font(.title) + .foregroundColor(.orange) + .lineLimit(2) + HStack { + PopularityBadge(score: score) + Text(releaseDate.map(formatter.string(from:)) ?? "TBD") + .font(.subheadline) + .foregroundColor(.primary) + .lineLimit(1) + } + Text(overview) + .foregroundColor(.secondary) + .lineLimit(3) + } + .padding(.leading, 8) + } + .padding(.top, 8) + .padding(.bottom, 8) + .contentShape(Rectangle()) + } + + public enum Size: String { + case small = "https://image.tmdb.org/t/p/w154/" + case medium = "https://image.tmdb.org/t/p/w500/" + case cast = "https://image.tmdb.org/t/p/w185/" + case original = "https://image.tmdb.org/t/p/original/" + + func path(poster: String) -> URL { + return URL(string: rawValue)!.appendingPathComponent(poster) + } + } +} + +private let formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + return formatter +}() diff --git a/Projects/UI/UI/Components/PopularityBadge.swift b/Projects/UI/UI/Components/PopularityBadge.swift new file mode 100644 index 00000000..ae5fbd67 --- /dev/null +++ b/Projects/UI/UI/Components/PopularityBadge.swift @@ -0,0 +1,61 @@ +import SwiftUI + +public struct PopularityBadge: View { + public let score: Int + + @State private var isDisplayed = false + + public init(score: Int) { + self.score = score + } + + var scoreColor: Color { + if score < 40 { + return .red + } else if score < 60 { + return .orange + } else if score < 75 { + return .yellow + } + return .green + } + + var overlay: some View { + ZStack { + Circle() + .trim(from: 0, + to: isDisplayed ? CGFloat(score) / 100 : 0) + .stroke(style: StrokeStyle(lineWidth: 2, dash: [1])) + .foregroundColor(scoreColor) + } + .rotationEffect(.degrees(-90)) + .onAppear { + withAnimation { + self.isDisplayed = true + } + } + } + + public var body: some View { + ZStack { + Circle() + .foregroundColor(.clear) + .frame(width: 40) + .overlay(overlay) + .shadow(color: scoreColor, radius: 4) + Text("\(score)%") + .font(Font.system(size: 10)) + .fontWeight(.bold) + } + .frame(width: 40, height: 40) + } +} + +#Preview { + HStack { + PopularityBadge(score: 35) + PopularityBadge(score: 50) + PopularityBadge(score: 70) + PopularityBadge(score: 90) + } +} diff --git a/Projects/UI/UI/Components/PosterImageView.swift b/Projects/UI/UI/Components/PosterImageView.swift new file mode 100644 index 00000000..320933de --- /dev/null +++ b/Projects/UI/UI/Components/PosterImageView.swift @@ -0,0 +1,77 @@ +import SwiftUI + +public struct PosterImageView: View { + let posterSize: PosterStyle.Size + let posterURL: URL? + + public init(posterSize: PosterStyle.Size, posterURL: URL?) { + self.posterSize = posterSize + self.posterURL = posterURL + } + + public var body: some View { + if let posterURL = posterURL { + URLImage(url: posterURL) { content in + switch content { + case .success(let image): + image.resizable() + .renderingMode(.original) + .transition(.opacity) + .posterStyle(loaded: true, size: posterSize) + case .failure, .empty: + Rectangle() + .foregroundColor(.gray) + .posterStyle(loaded: false, size: posterSize) + @unknown default: + Rectangle() + .foregroundColor(.gray) + .posterStyle(loaded: false, size: posterSize) + } + } + } else { + Rectangle() + .foregroundColor(.gray) + .posterStyle(loaded: false, size: posterSize) + } + } +} + +public struct PosterStyle: ViewModifier { + public enum Size { + case small, medium, big, tv + + public var width: CGFloat { + switch self { + case .small: return 53 + case .medium: return 100 + case .big: return 250 + case .tv: return 333 + } + } + + public var height: CGFloat { + switch self { + case .small: return 80 + case .medium: return 150 + case .big: return 375 + case .tv: return 500 + } + } + } + + let loaded: Bool + let size: Size + + public func body(content: Content) -> some View { + return content + .frame(width: size.width, height: size.height) + .cornerRadius(5) + .opacity(loaded ? 1 : 0.1) + } +} + +extension View { + func posterStyle(loaded: Bool, size: PosterStyle.Size) -> some View { + return ModifiedContent(content: self, modifier: PosterStyle(loaded: loaded, size: size)) + } +} diff --git a/Projects/UI/UI/Components/RoundedBadge.swift b/Projects/UI/UI/Components/RoundedBadge.swift new file mode 100644 index 00000000..0024f531 --- /dev/null +++ b/Projects/UI/UI/Components/RoundedBadge.swift @@ -0,0 +1,33 @@ +import SwiftUI + +public struct RoundedBadge: View { + public let text: String + public let color: Color + + public init(text: String, color: Color) { + self.text = text + self.color = color + } + + public var body: some View { + HStack { + Text(text.capitalized) + .font(.footnote) + .fontWeight(.bold) + .foregroundColor(.primary) + .padding(.leading, 10) + .padding([.top, .bottom], 5) + Image(systemName: "chevron.right") + .resizable() + .frame(width: 5, height: 10) + .foregroundColor(.primary) + .padding(.trailing, 10) + } + .background( + Rectangle() + .foregroundColor(color) + .cornerRadius(12) + ) + .padding(.bottom, 4) + } +} diff --git a/Projects/UI/UI/Components/URLImage.swift b/Projects/UI/UI/Components/URLImage.swift new file mode 100644 index 00000000..8d0105fe --- /dev/null +++ b/Projects/UI/UI/Components/URLImage.swift @@ -0,0 +1,91 @@ +import SwiftUI + +public struct URLImage: View { + private var model: URLImageModel + private let content: (AsyncImagePhase) -> Content + + public init(url: URL, @ViewBuilder content: @escaping (AsyncImagePhase) -> Content) { + self.model = URLImageModel(url: url) + self.content = content + } + + public var body: some View { + content(model.image) + .task { + await model.load() + } + } +} + +@Observable +final class URLImageModel: ObservableObject { + var image: AsyncImagePhase + + private let url: URL + private let cache: ImageCache + private let loadImage: (URL) async throws -> (Data, URLResponse) + + init(url: URL, + cache: ImageCache = .shared, + loadImage: @escaping (URL) async throws -> (Data, URLResponse) = URLSession.shared.data(from:)) + { + self.url = url + self.cache = cache + self.loadImage = loadImage + + self.image = cache.image(for: url) + .map(Image.init(uiImage:)) + .map(AsyncImagePhase.success) ?? .empty + } + + @MainActor + func load() async { + if case .success = image { + return + } + do { + let (data, _) = try await loadImage(url) + guard let image = UIImage(data: data) else { + self.image = .empty + return + } + cache.set(image: image, url: url) + self.image = .success(Image(uiImage: image)) + } catch { + image = .failure(error) + } + } +} + +final class ImageCache { + private let cache = NSCache() + + static let shared = ImageCache() + + func image(for url: URL) -> UIImage? { + return cache.object(forKey: Key(url: url)) + } + + func set(image: UIImage, url: URL) { + cache.setObject(image, forKey: Key(url: url)) + } + + private final class Key: NSObject { + private let url: URL + + init(url: URL) { + self.url = url + } + + override func isEqual(_ object: Any?) -> Bool { + guard let object = object as? Key else { + return false + } + return url == object.url + } + + override var hash: Int { + return url.hashValue + } + } +} diff --git a/Projects/UI/UI/Components/ViewDidLoad.swift b/Projects/UI/UI/Components/ViewDidLoad.swift new file mode 100644 index 00000000..f5c2a054 --- /dev/null +++ b/Projects/UI/UI/Components/ViewDidLoad.swift @@ -0,0 +1,33 @@ +import SwiftUI + +public extension View { + func onViewDidLoad(didLoad: @escaping () -> Void) -> some View { + modifier(ViewDidLoad(didLoad: didLoad)) + } +} + +struct ViewDidLoad: ViewModifier { + @StateObject var model: Model + + init(didLoad: @escaping () -> Void) { + _model = StateObject(wrappedValue: Model(didLoad: didLoad)) + } + + func body(content: Content) -> some View { + content.onAppear { + if model.didCallAppear == false { + model.didCallAppear = true + model.didLoad() + } + } + } + + final class Model: ObservableObject { + var didCallAppear = false + let didLoad: () -> Void + + init(didLoad: @escaping () -> Void) { + self.didLoad = didLoad + } + } +} diff --git a/Projects/UI/UI/Components/WebView.swift b/Projects/UI/UI/Components/WebView.swift new file mode 100644 index 00000000..7ca5e744 --- /dev/null +++ b/Projects/UI/UI/Components/WebView.swift @@ -0,0 +1,19 @@ +import SwiftUI +import UIKit +import WebKit + +public struct WebView: UIViewRepresentable { + let url: URL + + public init(url: URL) { + self.url = url + } + + public func makeUIView(context: Context) -> WKWebView { + WKWebView(frame: .zero, configuration: .init()) + } + + public func updateUIView(_ uiView: WKWebView, context: Context) { + uiView.load(URLRequest(url: url)) + } +} diff --git a/Projects/UI/UI/ErrorHandling/ErrorCoordinator.swift b/Projects/UI/UI/ErrorHandling/ErrorCoordinator.swift new file mode 100644 index 00000000..c77758d7 --- /dev/null +++ b/Projects/UI/UI/ErrorHandling/ErrorCoordinator.swift @@ -0,0 +1,69 @@ +import Dependencies +import PopupView +import SwiftUI + +// Open so it could be ovveriden for Unit Tests +// as @Observable does not work with protocols +@Observable +open class ErrorToastCoordinator { + var shouldShow: Bool = false + + open func show() { + shouldShow = true + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.shouldShow = false + } + } +} + +extension ErrorToastCoordinator: DependencyKey { + public static var liveValue: ErrorToastCoordinator { + ErrorToastCoordinator() + } +} + +public extension DependencyValues { + var errorToastCoordinator: ErrorToastCoordinator { + get { + self[ErrorToastCoordinator.self] + } + set { + self[ErrorToastCoordinator.self] = newValue + } + } +} + +struct ErrorShowing: ViewModifier { + @Bindable var coordinator: ErrorToastCoordinator + + init() { + @Dependency(\.errorToastCoordinator) var coordinator + self.coordinator = coordinator + } + + func body(content: Content) -> some View { + content.popup(isPresented: $coordinator.shouldShow) { + ErrorToast() + } customize: { + $0 + .type(.toast) + .position(.top) + } + } +} + +struct ErrorToast: View { + var body: some View { + Text("Somethign went wrong") + .foregroundColor(.white) + .padding(EdgeInsets(top: 60, leading: 32, bottom: 16, trailing: 32)) + .frame(maxWidth: .infinity) + .background(Color.red) + } +} + +public extension View { + func errorShowing() -> some View { + modifier(ErrorShowing()) + } +} diff --git a/Projects/UI/UI/TCAExtensions/Fetch.swift b/Projects/UI/UI/TCAExtensions/Fetch.swift new file mode 100644 index 00000000..b6cf62e7 --- /dev/null +++ b/Projects/UI/UI/TCAExtensions/Fetch.swift @@ -0,0 +1,108 @@ +import ComposableArchitecture +import Foundation + +@Reducer +public struct Fetch { + let cancelToken: any Hashable + let fetch: (Params) async throws -> Success + + public init(cancelToken: any Hashable = UUID().uuidString, fetch: @escaping (Params) async throws -> Success) { + self.cancelToken = cancelToken + self.fetch = fetch + } + + public var body: some Reducer { + Reduce { state, action in + switch action { + case .fetch(let params): + state = .fetching + return .run { send in + do { + let response = try await self.fetch(params) + await send(.response(response)) + } catch { + await send(.failure(AnyError(error: error))) + } + } + .cancellable(id: cancelToken, cancelInFlight: true) + case .response(let response): + state = .success(response) + return .none + case .failure(let error): + state = .failure(error) + return .none + case .cancel: + return .cancel(id: cancelToken) + } + } + } + + public enum State: Equatable { + case notInitiated + case fetching + case success(Success) + case failure(AnyError) + + public var fetched: Success? { + switch self { + case .notInitiated, .fetching: + return nil + case .success(let response): + return response + case .failure: + return nil + } + } + + public var isFetching: Bool { + switch self { + case .notInitiated, .success, .failure: + return false + case .fetching: + return true + } + } + } + + public enum Action: Equatable { + case fetch(Params) + case response(Success) + case failure(AnyError) + case cancel + } + + public func errorHandling() -> ErrorHandling { + ErrorHandling(fetch: self) + } +} + +public struct AnyError: Error, Equatable { + let error: Error + + public static func == (lhs: AnyError, rhs: AnyError) -> Bool { + (lhs as NSError) == (rhs.error as NSError) + } +} + +public struct ErrorHandling: Reducer { + public typealias State = Fetch.State + public typealias Action = Fetch.Action + + let fetch: Fetch + + @Dependency(\.errorToastCoordinator) + private var errorToast + + public var body: some Reducer { + fetch + Reduce { state, action in + switch action { + case .failure: + errorToast.show() + return .none + default: + return .none + } + } + } +} diff --git a/README.md b/README.md index b0b17f38..5e0b9b81 100644 --- a/README.md +++ b/README.md @@ -1,269 +1,111 @@ -# Clean architecture with [RxSwift](https://github.com/ReactiveX/RxSwift) - -## Contributions are welcome and highly appreciated!! -You can do this by: - -- opening an issue to discuss the current solution, ask a question, propose your solution etc. (also English is not my native language so if you think that something can be corrected please open a PR 😊) -- opening a PR if you want to fix bugs or improve something - -### Instalation - -Dependencies in this project are provided via Cocoapods. Please install all dependecies with - -` -pod install -` - -## High level overview -![](Architecture/Modules.png) - -#### Domain - - -The `Domain` is basically what is your App about and what it can do (Entities, UseCase etc.) **It does not depend on UIKit or any persistence framework**, and it doesn't have implementations apart from entities - -#### Platform - -The `Platform` is a concrete implementation of the `Domain` in a specific platform like iOS. It does hide all implementation details. For example Database implementation whether it is CoreData, Realm, SQLite etc. - -#### Application -`Application` is responsible for delivering information to the user and handling user input. It can be implemented with any delivery pattern e.g (MVVM, MVC, MVP). This is the place for your `UIView`s and `UIViewController`s. As you will see from the example app, `ViewControllers` are completely independent of the `Platform`. The only responsibility of a view controller is to "bind" the UI to the Domain to make things happen. In fact, in the current example we are using the same view controller for Realm and CoreData. - - -## Detail overview -![](Architecture/ModulesDetails.png) - -To enforce modularity, `Domain`, `Platform` and `Application` are separate targets in the App, which allows us to take advantage of the `internal` access layer in Swift to prevent exposing of types that we don't want to expose. - -#### Domain - -Entities are implemented as Swift value types - -```swift -public struct Post { - public let uid: String - public let createDate: Date - public let updateDate: Date - public let title: String - public let content: String -} -``` - -UseCases are protocols which do one specific thing: - -```swift - -public protocol PostsUseCase { - func posts() -> Observable<[Post]> - func save(post: Post) -> Observable -} - -``` - -`UseCaseProvider` is a [service locator](https://en.wikipedia.org/wiki/Service_locator_pattern). In the current example, it helps to hide the concrete implementation of use cases. - -#### Platform - -In some cases, we can't use Swift structs for our domain objects because of DB framework requirements (e.g. CoreData, Realm). - -```swift -final class CDPost: NSManagedObject { - @NSManaged public var uid: String? - @NSManaged public var title: String? - @NSManaged public var content: String? - @NSManaged public var createDate: NSDate? - @NSManaged public var updateDate: NSDate? -} - -final class RMPost: Object { - dynamic var uid: String = "" - dynamic var createDate: NSDate = NSDate() - dynamic var updateDate: NSDate = NSDate() - dynamic var title: String = "" - dynamic var content: String = "" -} - -``` - - -The `Platform` also contains concrete implementations of your use cases, repositories or any services that are defined in the `Domain`. - -```swift -final class PostsUseCase: Domain.PostsUseCase { - - private let repository: AbstractRepository - - init(repository: AbstractRepository) { - self.repository = repository - } - - func posts() -> Observable<[Post]> { - return repository.query(sortDescriptors: [Post.CoreDataType.uid.descending()]) - } - - func save(post: Post) -> Observable { - return repository.save(entity: post) - } -} - -final class Repository: AbstractRepository where T == T.CoreDataType.DomainType { - private let context: NSManagedObjectContext - private let scheduler: ContextScheduler - - init(context: NSManagedObjectContext) { - self.context = context - self.scheduler = ContextScheduler(context: context) - } - - override func query(with predicate: NSPredicate? = nil, - sortDescriptors: [NSSortDescriptor]? = nil) -> Observable<[T]> { - let request = T.CoreDataType.fetchRequest() - request.predicate = predicate - request.sortDescriptors = sortDescriptors - return context.rx.entities(fetchRequest: request) - .mapToDomain() - .subscribeOn(scheduler) - } - - override func save(entity: T) -> Observable { - return entity.sync(in: context) - .mapToVoid() - .flatMapLatest(context.rx.save) - .subscribeOn(scheduler) - } -} -``` - -As you can see, concrete implementations are internal, because we don't want to expose our dependecies. The only thing that is exposed in the current example from the `Platform` is a concrete implementation of the `UseCaseProvider`. - -```swift -public final class UseCaseProvider: Domain.UseCaseProvider { - private let coreDataStack = CoreDataStack() - private let postRepository: Repository - - public init() { - postRepository = Repository(context: coreDataStack.context) - } - - public func makePostsUseCase() -> Domain.PostsUseCase { - return PostsUseCase(repository: postRepository) - } -} -``` - -#### Application - -In the current example, `Application` is implemented with the [MVVM](https://en.wikipedia.org/wiki/Model–view–viewmodel) pattern and heavy use of [RxSwift](https://github.com/ReactiveX/RxSwift), which makes binding very easy. - -![](Architecture/MVVMPattern.png) - -Where the `ViewModel` performs pure transformation of a user `Input` to the `Output` - -```swift - -protocol ViewModelType { - associatedtype Input - associatedtype Output - - func transform(input: Input) -> Output -} -``` - - -```swift -final class PostsViewModel: ViewModelType { - struct Input { - let trigger: Driver - let createPostTrigger: Driver - let selection: Driver - } - struct Output { - let fetching: Driver - let posts: Driver<[Post]> - let createPost: Driver - let selectedPost: Driver - let error: Driver - } - - private let useCase: AllPostsUseCase - private let navigator: PostsNavigator - - init(useCase: AllPostsUseCase, navigator: PostsNavigator) { - self.useCase = useCase - self.navigator = navigator - } - - func transform(input: Input) -> Output { - ...... - } -``` - -A `ViewModel` can be injected into a `ViewController` via property injection or initializer. In the current example, this is done by `Navigator`. - -```swift - -protocol PostsNavigator { - func toCreatePost() - func toPost(_ post: Post) - func toPosts() -} - -class DefaultPostsNavigator: PostsNavigator { - private let storyBoard: UIStoryboard - private let navigationController: UINavigationController - private let services: ServiceLocator - - init(services: ServiceLocator, - navigationController: UINavigationController, - storyBoard: UIStoryboard) { - self.services = services - self.navigationController = navigationController - self.storyBoard = storyBoard - } - - func toPosts() { - let vc = storyBoard.instantiateViewController(ofType: PostsViewController.self) - vc.viewModel = PostsViewModel(useCase: services.getAllPostsUseCase(), - navigator: self) - navigationController.pushViewController(vc, animated: true) - } - .... -} - -class PostsViewController: UIViewController { - private let disposeBag = DisposeBag() - - var viewModel: PostsViewModel! - - ... -} -``` - -### Example - -The example app is Post/TODOs app which uses `Realm`, `CoreData` and `Network` at the same time as a proof of concept that the `Application` level is not dependant on the Platform level implementation details. - -| CoreData | Realm | Network | -| -------- | ----- | ------- | -|![](Architecture/CoreData.gif) | ![](Architecture/Realm.gif) | ![](Architecture/Network.gif) | - -### Modularization - -The corner stone of **Clean Architecture** is modularization, as you can hide implementation detail under `internal` access layer. Further read of this topic [here](https://github.com/microfeatures/guidelines) - -### TODO: - -* add tests -* add [MVP](https://en.wikipedia.org/wiki/Model–view–presenter) example -* [Redux](http://redux.js.org) example?? - -### Links -* [RxSwift](https://github.com/ReactiveX/RxSwift) -* [RxSwift Book](https://store.raywenderlich.com/products/rxswift) -* [Robert C Martin - Clean Architecture and Design](https://www.youtube.com/watch?v=Nsjsiz2A9mg) -* [Cycle.js](https://cycle.js.org) -* [ViewModel](https://medium.com/@SergDort/viewmodel-in-rxswift-world-13d39faa2cf5#.qse37r6jw) in Rx world - -### Any questions? - -* ping me on [Twitter](https://twitter.com/SergDort) +# Modern Clean Architecture + +## Motivation + +The purpose of this repository is to serve as a comprehensive resource for iOS developers seeking to understand +and apply the principles of Domain-Driven Design (DDD) and Clean Architecture in their software projects. The modularization technics are heavily inspired by these two books [Domain-Driven Design: Tackling Complexity in the Heart of Software](https://amzn.eu/d/etWIrog), [Clean Architecture](https://amzn.eu/d/477lqj8) + +## Example + +As an example we going to build an app that lets us explore [Movies](https://developer.themoviedb.org/reference) and [Anime](https://anilist.co) API. + +| Movies | Movie Detail | Anime | Anime Detail | My Lists | +| --- | --- | --- | --- | --- | +| ![](images/movies_list.gif) | ![](images/movie_detail.gif) | ![](images/anime_list.gif) | ![](images/anime_detail.gif) | ![](images/mylists.gif) | + +## How to run the example + +You will need to install the Tuist first. You can follow their official [documentation](https://docs.tuist.dev/en/guides/quick-start/install-tuist) on how do do that. Once you've got tuist installed. You need to run two commands in the root of the project folder + +``` +tuist install +tuist generate +``` + +## Layered Archtecture + +Layered Archtecture + + +![](images/layers_responsibility.png) + +> The application also makes no assumptions about the source of the transfer request. The program +presumably includes a UI with entry fields for account numbers and amounts and with buttons for +commands. But that user interface could be replaced by a wire request in XML without affecting the +application layer or any of the lower layers. **This decoupling is important not because projects frequently need to replace user interfaces with wire requests but because a clean separation of concerns keeps the design of each layer easy to understand and maintain.** +> +> Domain-Driven Design: Tackling Complexity in the Heart of Software + +**In the context of iOS development and our example application, the layers are structured as follows:** + + + +## Responsibilities + +### Domain + +Domain Layer plays a central role by encapsulating the core business logic and rules of the application. Its purpose is to model the problem domain effectively and ensure that business requirements are met in a clear and maintainable way. It includes entities, value objects, and aggregates that represent concepts within the business domain. It contains UseCase definition and implementation. By isolating business logic within the Domain Layer, we ensure that application-specific concerns (e.g., UI, infrastructure) don’t pollute the domain model. + +### Features + +The Features layer is responsible for implementing UI business requirements, building individual screens, and coordinating Domain UseCases by invoking their various methods. As an example I implemented `Movies` feature using MVVM design pattern which with introduction of SwiftUI feets nicely into iOS ecosystem. + +![](images/MVVMPattern.png) + +Another example of a UI design pattern is my use of [TCA](https://github.com/pointfreeco/swift-composable-architecture) to implement the Anime feature. TCA, being an excellent implementation of a [Finite State Machine](https://en.wikipedia.org/wiki/Finite-state_machine#:~:text=It%20is%20an%20abstract%20machine,another%20is%20called%20a%20transition.), integrates seamlessly into the SwiftUI ecosystem. +The purpose of these two examples is to demonstrate that with proper modularization and decoupling, the choice of UI architecture pattern becomes less critical, as each has its own advantages and trade-offs. + +For navigation abstraction, I used the Coordinator pattern, which, at its core, is essentially just the Delegate pattern. From the perspective of a ViewModel or Reducer, navigation is simply delegated as a side effect to another object. With the addition of APIs like [NavigationStack](https://developer.apple.com/documentation/swiftui/navigationstack), SwiftUI’s navigation abstraction has become more complete, making navigation implementation [much simpler](https://github.com/sergdort/ModernCleanArchitecture/blob/master/Projects/Example/Sources/Navigation/Movies/MoviesNavigationView.swift). + +```swift +@MainActor +public protocol MoviesCoordinator { + func showDetail(for movie: Movie) + func showDetail(for person: Person) + func showAddMovieToCustomList(for movie: Movie) +} + +public final class MoviesViewModel { + @ObservationIgnored + private let coordinator: MoviesCoordinator + + func didSelect(movie: Movie) { + coordinator.showDetail(for: movie) + } +} +``` + +In terms of dependencies, feature modules rely solely on the Domain layer and the UI. The concrete implementations of UseCases are injected at runtime by the Application layer. + + + +### Application + +The Application layer is responsible for creating the main user interface, setting up and instantiating concrete implementations of UseCases, and injecting them using the [@Dependecy](https://github.com/pointfreeco/swift-dependencies) library. Another key responsibility of the Application layer is managing navigation and injecting dependencies into the Features layer. +You might wonder why the Application layer should handle navigation instead of the features themselves. The answer lies in the Application layer's responsibility: it is tasked with creating the main user interface, which inherently provides it with a complete understanding of the navigation hierarchy and the context necessary to manage navigation effectively. + +### Platform and "Plugin architecture" +> From the Robert C. Martin "Clean Architecture" +> + + +The purpose of the Platform layer is to utilize the Core layer's implementation and the Domain business rules (e.g., Entities and UseCases) to provide concrete implementations. For example, the MoviesAPI leverages the MovieDomain and the HTTPClient module to implement MoviesGateway, enabling interaction with the MoviesDBAPI. Similarly, the MoviesDB module uses FileCache or SwiftData to persist previously downloaded data. + +This approach supports a plugin design, allowing for flexibility to swap or modify the underlying implementations of the database or API as needed. For instance, the AnimeAPI uses GraphQL to fetch data, and this design ensures that implementation details remain hidden from the Features layer. This approach is improrant not because we will be changing our DB or API every other day, but because it allows us to decouble our business rules from the concrete implementaion making it easier to maintain. + + + +### Core + +The Core layer is responsible for implementing the foundational infrastructure of the application, such as the HTTP library, SwiftData extensions, Apollo extensions, FileCache, and other essential utilities. + +## Developer Productivity + +One of the most critical aspects of good architecture is to enable the team to be productive. Let’s explore how effective modularization techniques and the Dependency Inversion Principle can significantly reduce Feature modules build times. As shown earlier, Feature module depends only on the UI and Domain modules, day-to-day development tasks can take advantage of Xcode Previews or feature-specific example apps, eliminating the need to build most of the application. + + + + + + + diff --git a/RealmPlatform/Entities/Convertion/DomainConvertibleType.swift b/RealmPlatform/Entities/Convertion/DomainConvertibleType.swift deleted file mode 100644 index c71fb046..00000000 --- a/RealmPlatform/Entities/Convertion/DomainConvertibleType.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -protocol DomainConvertibleType { - associatedtype DomainType - - func asDomain() -> DomainType -} diff --git a/RealmPlatform/Entities/Convertion/RealmRepresentable.swift b/RealmPlatform/Entities/Convertion/RealmRepresentable.swift deleted file mode 100644 index 5853df21..00000000 --- a/RealmPlatform/Entities/Convertion/RealmRepresentable.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation - -protocol RealmRepresentable { - associatedtype RealmType: DomainConvertibleType - - var uid: String {get} - - func asRealm() -> RealmType -} diff --git a/RealmPlatform/Entities/RMAddress.swift b/RealmPlatform/Entities/RMAddress.swift deleted file mode 100644 index 681b03eb..00000000 --- a/RealmPlatform/Entities/RMAddress.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// RMAddress.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import QueryKit -import Domain -import RealmSwift -import Realm - -final class RMAddress: Object { - - @objc dynamic var city: String = "" - @objc dynamic var geo: RMLocation? - @objc dynamic var street: String = "" - @objc dynamic var suite: String = "" - @objc dynamic var zipcode: String = "" -} - -extension RMAddress { - static var city: Attribute { return Attribute("city")} - static var street: Attribute { return Attribute("street")} - static var suite: Attribute { return Attribute("suite")} - static var zipcode: Attribute { return Attribute("zipcode")} - static var geo: Attribute { return Attribute("geo")} -} - -extension RMAddress: DomainConvertibleType { - func asDomain() -> Address { - return Address(city: city, - geo: geo!.asDomain(), - street: street, - suite: suite, - zipcode: zipcode) - } -} - -extension Address: RealmRepresentable { - internal var uid: String { - return "" - } - - func asRealm() -> RMAddress { - return RMAddress.build { object in - object.city = city - object.geo = geo.asRealm() - object.street = street - object.suite = suite - object.zipcode = zipcode - } - } -} diff --git a/RealmPlatform/Entities/RMAlbum.swift b/RealmPlatform/Entities/RMAlbum.swift deleted file mode 100644 index dadc85ef..00000000 --- a/RealmPlatform/Entities/RMAlbum.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// RMAlbum.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import QueryKit -import Domain -import RealmSwift -import Realm - -final class RMAlbum: Object { - - @objc dynamic var title: String = "" - @objc dynamic var uid: String = "" - @objc dynamic var userId: String = "" - - override class func primaryKey() -> String? { - return "uid" - } -} - -extension RMAlbum { - static var title: Attribute { return Attribute("title")} - static var userId: Attribute { return Attribute("userId")} - static var uid: Attribute { return Attribute("uid")} -} - -extension RMAlbum: DomainConvertibleType { - func asDomain() -> Album { - return Album(title: title, uid: uid, userId: userId) - } -} - -extension Album: RealmRepresentable { - - func asRealm() -> RMAlbum { - return RMAlbum.build { object in - object.title = title - object.uid = uid - object.userId = userId - } - } -} diff --git a/RealmPlatform/Entities/RMComment.swift b/RealmPlatform/Entities/RMComment.swift deleted file mode 100644 index 5f6ab913..00000000 --- a/RealmPlatform/Entities/RMComment.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// RMComment.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import QueryKit -import Domain -import RealmSwift -import Realm - -final class RMComment: Object { - - @objc dynamic var body: String = "" - @objc dynamic var email: String = "" - @objc dynamic var name: String = "" - @objc dynamic var postId: String = "" - @objc dynamic var uid: String = "" - - override class func primaryKey() -> String? { - return "uid" - } -} - -extension RMComment { - static var body: Attribute { return Attribute("body")} - static var email: Attribute { return Attribute("email")} - static var name: Attribute { return Attribute("name")} - static var postId: Attribute { return Attribute("postId")} - static var uid: Attribute { return Attribute("uid")} -} - -extension RMComment: DomainConvertibleType { - func asDomain() -> Comment { - return Comment(body: body, - email: email, - name: name, - postId: postId, - uid: uid) - } -} - -extension Comment: RealmRepresentable { - - func asRealm() -> RMComment { - return RMComment.build { object in - object.body = body - object.email = email - object.name = name - object.uid = uid - object.postId = postId - } - } -} diff --git a/RealmPlatform/Entities/RMCompany.swift b/RealmPlatform/Entities/RMCompany.swift deleted file mode 100644 index 26a5aaff..00000000 --- a/RealmPlatform/Entities/RMCompany.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// RMCompany.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import QueryKit -import Domain -import RealmSwift -import Realm - -final class RMCompany: Object { - - @objc dynamic var bs: String = "" - @objc dynamic var catchPhrase: String = "" - @objc dynamic var name: String = "" -} - -extension RMCompany { - static var bs: Attribute { return Attribute("bs")} - static var catchPhrase: Attribute { return Attribute("catchPhrase")} - static var name: Attribute { return Attribute("name")} -} - -extension RMCompany: DomainConvertibleType { - func asDomain() -> Company { - return Company(bs: bs, - catchPhrase: catchPhrase, - name: name) - } -} - -extension Company: RealmRepresentable { - internal var uid: String { - return "" - } - - func asRealm() -> RMCompany { - return RMCompany.build { object in - object.bs = bs - object.catchPhrase = catchPhrase - object.name = name - } - } -} diff --git a/RealmPlatform/Entities/RMLocation.swift b/RealmPlatform/Entities/RMLocation.swift deleted file mode 100644 index 656e7d17..00000000 --- a/RealmPlatform/Entities/RMLocation.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation -import QueryKit -import Domain -import RealmSwift -import Realm - -final class RMLocation: Object { - @objc dynamic var latitude: Double = 0 - @objc dynamic var longitude: Double = 0 -} - -extension RMLocation { - static var latitude: Attribute { return Attribute("latitude")} - static var longitude: Attribute { return Attribute("longitude")} -} - -extension RMLocation: DomainConvertibleType { - func asDomain() -> Location { - return Location(latitude: latitude, - longitude: longitude) - } -} - -extension Location: RealmRepresentable { - internal var uid: String { - return "" - } - - func asRealm() -> RMLocation { - return RMLocation.build { object in - object.latitude = latitude - object.longitude = longitude - } - } -} diff --git a/RealmPlatform/Entities/RMPhoto.swift b/RealmPlatform/Entities/RMPhoto.swift deleted file mode 100644 index 05d47f83..00000000 --- a/RealmPlatform/Entities/RMPhoto.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// RMPhoto.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import QueryKit -import Domain -import RealmSwift -import Realm - -final class RMPhoto: Object { - @objc dynamic var albumId: String = "" - @objc dynamic var thumbnailUrl: String = "" - @objc dynamic var title: String = "" - @objc dynamic var uid: String = "" - @objc dynamic var url: String = "" - - override class func primaryKey() -> String? { - return "uid" - } -} - -extension RMPhoto { - static var title: Attribute { return Attribute("title")} - static var thumbnailUrl: Attribute { return Attribute("thumbnailUrl")} - static var url: Attribute { return Attribute("url")} - static var albumId: Attribute { return Attribute("albumId")} - static var uid: Attribute { return Attribute("uid")} -} - -extension RMPhoto: DomainConvertibleType { - func asDomain() -> Photo { - return Photo(albumId: albumId, - thumbnailUrl: thumbnailUrl, - title: title, - uid: uid, - url: url) - } -} - -extension Photo: RealmRepresentable { - - func asRealm() -> RMPhoto { - return RMPhoto.build { object in - object.albumId = albumId - object.thumbnailUrl = thumbnailUrl - object.title = title - object.uid = uid - object.url = url - } - } -} diff --git a/RealmPlatform/Entities/RMPost.swift b/RealmPlatform/Entities/RMPost.swift deleted file mode 100644 index 62d99ace..00000000 --- a/RealmPlatform/Entities/RMPost.swift +++ /dev/null @@ -1,46 +0,0 @@ -import QueryKit -import Domain -import RealmSwift -import Realm - -final class RMPost: Object { - @objc dynamic var uid: String = "" - @objc dynamic var userId: String = "" - @objc dynamic var title: String = "" - @objc dynamic var body: String = "" - @objc dynamic var createdAt: String = "" - - override class func primaryKey() -> String? { - return "uid" - } -} - -extension RMPost { - static var title: Attribute { return Attribute("title")} - static var body: Attribute { return Attribute("body")} - static var userId: Attribute { return Attribute("userId")} - static var uid: Attribute { return Attribute("uid")} - static var createdAt: Attribute { return Attribute("createdAt")} -} - -extension RMPost: DomainConvertibleType { - func asDomain() -> Post { - return Post(body: body, - title: title, - uid: uid, - userId: userId, - createdAt: createdAt) - } -} - -extension Post: RealmRepresentable { - func asRealm() -> RMPost { - return RMPost.build { object in - object.uid = uid - object.userId = userId - object.title = title - object.body = body - object.createdAt = createdAt - } - } -} diff --git a/RealmPlatform/Entities/RMTodo.swift b/RealmPlatform/Entities/RMTodo.swift deleted file mode 100644 index 32dbccb5..00000000 --- a/RealmPlatform/Entities/RMTodo.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// RMTodo.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import QueryKit -import Domain -import RealmSwift -import Realm - -final class RMTodo: Object { - - @objc dynamic var completed: Bool = false - @objc dynamic var title: String = "" - @objc dynamic var uid: String = "" - @objc dynamic var userId: String = "" - - override class func primaryKey() -> String? { - return "uid" - } -} - -extension RMTodo { - static var title: Attribute { return Attribute("title")} - static var completed: Attribute { return Attribute("completed")} - static var userId: Attribute { return Attribute("userId")} - static var uid: Attribute { return Attribute("uid")} -} - -extension RMTodo: DomainConvertibleType { - func asDomain() -> Todo { - return Todo(completed: completed, - title: title, - uid: uid, - userId: userId) - } -} - -extension Todo: RealmRepresentable { - func asRealm() -> RMTodo { - return RMTodo.build { object in - object.uid = uid - object.userId = userId - object.title = title - object.completed = completed - } - } -} diff --git a/RealmPlatform/Entities/RMUser.swift b/RealmPlatform/Entities/RMUser.swift deleted file mode 100644 index d8126067..00000000 --- a/RealmPlatform/Entities/RMUser.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// RMUser.swift -// CleanArchitectureRxSwift -// -// Created by Andrey Yastrebov on 10.03.17. -// Copyright © 2017 sergdort. All rights reserved. -// - -import QueryKit -import Domain -import RealmSwift -import Realm - -final class RMUser: Object { - - @objc dynamic var address: RMAddress? - @objc dynamic var company: RMCompany? - @objc dynamic var email: String = "" - @objc dynamic var name: String = "" - @objc dynamic var phone: String = "" - @objc dynamic var uid: String = "" - @objc dynamic var username: String = "" - @objc dynamic var website: String = "" - - override class func primaryKey() -> String? { - return "uid" - } -} - -extension RMUser { - static var website: Attribute { return Attribute("website")} - static var email: Attribute { return Attribute("email")} - static var name: Attribute { return Attribute("name")} - static var phone: Attribute { return Attribute("phone")} - static var username: Attribute { return Attribute("username")} - static var uid: Attribute { return Attribute("uid")} - static var address: Attribute { return Attribute("address")} - static var company: Attribute { return Attribute("company")} -} - -extension RMUser: DomainConvertibleType { - func asDomain() -> User { - return User(address: address!.asDomain(), - company: company!.asDomain(), - email: email, - name: name, - phone: phone, - uid: uid, - username: username, - website: website) - } -} - -extension User: RealmRepresentable { - func asRealm() -> RMUser { - return RMUser.build { object in - object.uid = uid - object.address = address.asRealm() - object.company = company.asRealm() - object.email = email - object.name = name - object.phone = phone - object.username = username - object.website = website - } - } -} diff --git a/RealmPlatform/Info.plist b/RealmPlatform/Info.plist deleted file mode 100644 index fbe1e6b3..00000000 --- a/RealmPlatform/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - - - diff --git a/RealmPlatform/RealmPlatform.h b/RealmPlatform/RealmPlatform.h deleted file mode 100644 index fced934f..00000000 --- a/RealmPlatform/RealmPlatform.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// RealmPlatform.h -// RealmPlatform -// -// Created by sergdort on 18/02/2017. -// Copyright © 2017 sergdort. All rights reserved. -// - -#import - -//! Project version number for RealmPlatform. -FOUNDATION_EXPORT double RealmPlatformVersionNumber; - -//! Project version string for RealmPlatform. -FOUNDATION_EXPORT const unsigned char RealmPlatformVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/RealmPlatform/Repository/Repository.swift b/RealmPlatform/Repository/Repository.swift deleted file mode 100644 index 0f914725..00000000 --- a/RealmPlatform/Repository/Repository.swift +++ /dev/null @@ -1,70 +0,0 @@ -import Foundation -import Realm -import RealmSwift -import RxSwift -import RxRealm - -protocol AbstractRepository { - associatedtype T - func queryAll() -> Observable<[T]> - func query(with predicate: NSPredicate, - sortDescriptors: [NSSortDescriptor]) -> Observable<[T]> - func save(entity: T) -> Observable - func delete(entity: T) -> Observable -} - -final class Repository: AbstractRepository where T == T.RealmType.DomainType, T.RealmType: Object { - private let configuration: Realm.Configuration - private let scheduler: RunLoopThreadScheduler - - private var realm: Realm { - return try! Realm(configuration: self.configuration) - } - - init(configuration: Realm.Configuration) { - self.configuration = configuration - let name = "com.CleanArchitectureRxSwift.RealmPlatform.Repository" - self.scheduler = RunLoopThreadScheduler(threadName: name) - print("File 📁 url: \(RLMRealmPathForFile("default.realm"))") - } - - func queryAll() -> Observable<[T]> { - return Observable.deferred { - let realm = self.realm - let objects = realm.objects(T.RealmType.self) - - return Observable.array(from: objects) - .mapToDomain() - } - .subscribeOn(scheduler) - } - - func query(with predicate: NSPredicate, - sortDescriptors: [NSSortDescriptor] = []) -> Observable<[T]> { - return Observable.deferred { - let realm = self.realm - let objects = realm.objects(T.RealmType.self) -// The implementation is broken since we are not using predicate and sortDescriptors -// but it cause compiler to crash with xcode 8.3 ¯\_(ツ)_/¯ -// .filter(predicate) -// .sorted(by: sortDescriptors.map(SortDescriptor.init)) - - return Observable.array(from: objects) - .mapToDomain() - } - .subscribeOn(scheduler) - } - - func save(entity: T) -> Observable { - return Observable.deferred { - return self.realm.rx.save(entity: entity) - }.subscribeOn(scheduler) - } - - func delete(entity: T) -> Observable { - return Observable.deferred { - return self.realm.rx.delete(entity: entity) - }.subscribeOn(scheduler) - } - -} diff --git a/RealmPlatform/UseCases/PostsUseCase.swift b/RealmPlatform/UseCases/PostsUseCase.swift deleted file mode 100644 index 5a00f53f..00000000 --- a/RealmPlatform/UseCases/PostsUseCase.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation -import Domain -import RxSwift -import Realm -import RealmSwift - -final class PostsUseCase: Domain.PostsUseCase where Repository: AbstractRepository, Repository.T == Post { - - private let repository: Repository - - init(repository: Repository) { - self.repository = repository - } - - func posts() -> Observable<[Post]> { - return repository.queryAll() - } - - func save(post: Post) -> Observable { - return repository.save(entity: post) - } - - func delete(post: Post) -> Observable { - return repository.delete(entity: post) - } -} diff --git a/RealmPlatform/UseCases/UseCaseProvider.swift b/RealmPlatform/UseCases/UseCaseProvider.swift deleted file mode 100644 index b0fe4677..00000000 --- a/RealmPlatform/UseCases/UseCaseProvider.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation -import Domain -import Realm -import RealmSwift - -public final class UseCaseProvider: Domain.UseCaseProvider { - private let configuration: Realm.Configuration - - public init(configuration: Realm.Configuration = Realm.Configuration()) { - self.configuration = configuration - } - - public func makePostsUseCase() -> Domain.PostsUseCase { - let repository = Repository(configuration: configuration) - return PostsUseCase(repository: repository) - } -} diff --git a/RealmPlatform/Utility/Extensions/Observable+Ext.swift b/RealmPlatform/Utility/Extensions/Observable+Ext.swift deleted file mode 100644 index e855c530..00000000 --- a/RealmPlatform/Utility/Extensions/Observable+Ext.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import RxSwift - -extension Observable where Element: Sequence, Element.Iterator.Element: DomainConvertibleType { - typealias DomainType = Element.Iterator.Element.DomainType - - func mapToDomain() -> Observable<[DomainType]> { - return map { sequence -> [DomainType] in - return sequence.mapToDomain() - } - } -} - -extension Sequence where Iterator.Element: DomainConvertibleType { - typealias Element = Iterator.Element - func mapToDomain() -> [Element.DomainType] { - return map { - return $0.asDomain() - } - } -} diff --git a/RealmPlatform/Utility/Extensions/Realm+Ext.swift b/RealmPlatform/Utility/Extensions/Realm+Ext.swift deleted file mode 100644 index bdd11459..00000000 --- a/RealmPlatform/Utility/Extensions/Realm+Ext.swift +++ /dev/null @@ -1,54 +0,0 @@ -import Foundation -import Realm -import RealmSwift -import RxSwift - -extension Object { - static func build(_ builder: (O) -> () ) -> O { - let object = O() - builder(object) - return object - } -} - -extension SortDescriptor { - init(sortDescriptor: NSSortDescriptor) { - self.keyPath = sortDescriptor.key ?? "" - self.ascending = sortDescriptor.ascending - } -} - -extension Reactive where Base: Realm { - func save(entity: R, update: Bool = true) -> Observable where R.RealmType: Object { - return Observable.create { observer in - do { - try self.base.write { - self.base.add(entity.asRealm(), update: update) - } - observer.onNext(()) - observer.onCompleted() - } catch { - observer.onError(error) - } - return Disposables.create() - } - } - - func delete(entity: R) -> Observable where R.RealmType: Object { - return Observable.create { observer in - do { - guard let object = self.base.object(ofType: R.RealmType.self, forPrimaryKey: entity.uid) else { fatalError() } - - try self.base.write { - self.base.delete(object) - } - - observer.onNext(()) - observer.onCompleted() - } catch { - observer.onError(error) - } - return Disposables.create() - } - } -} diff --git a/RealmPlatform/Utility/RxUnits/RunLoopThreadScheduler.swift b/RealmPlatform/Utility/RxUnits/RunLoopThreadScheduler.swift deleted file mode 100644 index 0285c256..00000000 --- a/RealmPlatform/Utility/RxUnits/RunLoopThreadScheduler.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Foundation -import RxSwift - -final class RunLoopThreadScheduler: ImmediateSchedulerType { - private let thread: Thread - private let target: ThreadTarget - - init(threadName: String) { - self.target = ThreadTarget() - self.thread = Thread(target: target, - selector: #selector(ThreadTarget.threadEntryPoint), - object: nil) - self.thread.name = threadName - self.thread.start() - } - - func schedule(_ state: StateType, action: @escaping (StateType) -> Disposable) -> Disposable { - let disposable = SingleAssignmentDisposable() - - var action: Action? = Action { - if disposable.isDisposed { - return - } - disposable.setDisposable(action(state)) - } - - action?.perform(#selector(Action.performAction), - on: thread, - with: nil, - waitUntilDone: false, - modes: [RunLoop.Mode.default.rawValue]) - - let actionDisposable = Disposables.create { - action = nil - } - - return Disposables.create(disposable, actionDisposable) - } - - deinit { - thread.cancel() - } -} - -private final class ThreadTarget: NSObject { - @objc fileprivate func threadEntryPoint() { - let runLoop = RunLoop.current - runLoop.add(NSMachPort(), forMode: RunLoop.Mode.default) - runLoop.run() - } -} - -private final class Action: NSObject { - private let action: () -> () - - init(action: @escaping () -> ()) { - self.action = action - } - - @objc func performAction() { - action() - } -} diff --git a/RealmPlatformTests/Info.plist b/RealmPlatformTests/Info.plist deleted file mode 100644 index 6c6c23c4..00000000 --- a/RealmPlatformTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/RealmPlatformTests/RealmPlatformTests.swift b/RealmPlatformTests/RealmPlatformTests.swift deleted file mode 100644 index 7b35eacf..00000000 --- a/RealmPlatformTests/RealmPlatformTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// RealmPlatformTests.swift -// RealmPlatformTests -// -// Created by sergdort on 18/02/2017. -// Copyright © 2017 sergdort. All rights reserved. -// - -import XCTest -@testable import RealmPlatform - -class RealmPlatformTests: XCTestCase { - - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testPerformanceExample() { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/Tuist.swift b/Tuist.swift new file mode 100644 index 00000000..5e3d8d3e --- /dev/null +++ b/Tuist.swift @@ -0,0 +1,5 @@ +import ProjectDescription + +let config = Config( + generationOptions: .options(enforceExplicitDependencies: true) +) \ No newline at end of file diff --git a/Tuist/Package.resolved b/Tuist/Package.resolved new file mode 100644 index 00000000..d8654872 --- /dev/null +++ b/Tuist/Package.resolved @@ -0,0 +1,167 @@ +{ + "pins" : [ + { + "identity" : "apollo-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apollographql/apollo-ios.git", + "state" : { + "revision" : "99dfbdb869dbba16bd09e086b2b669bfcab811ec", + "version" : "1.12.2" + } + }, + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "5928286acce13def418ec36d05a001a9641086f2", + "version" : "1.0.3" + } + }, + { + "identity" : "popupview", + "kind" : "remoteSourceControl", + "location" : "https://github.com/exyte/PopupView.git", + "state" : { + "revision" : "e958d95620aff0227682cd8260c54cdbdfedc6c5", + "version" : "4.0.0" + } + }, + { + "identity" : "sqlite.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stephencelis/SQLite.swift.git", + "state" : { + "revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8", + "version" : "0.15.3" + } + }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "e7039aaa4d9cf386fa8324a89f258c3f2c54d751", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e", + "version" : "1.0.6" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" + } + }, + { + "identity" : "swift-composable-architecture", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-composable-architecture.git", + "state" : { + "revision" : "121b6080697b5457ac63e37d2f2d97cf5acd999a", + "version" : "1.13.1" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", + "version" : "1.3.3" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "85f89f5d0ce5a18945f65371d40ca997da85a41a", + "version" : "1.6.3" + } + }, + { + "identity" : "swift-identified-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-identified-collections", + "state" : { + "revision" : "2f5ab6e091dd032b63dacbda052405756010dc3b", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-navigation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-navigation", + "state" : { + "revision" : "e28911721538fa0c2439e92320bad13e3200866f", + "version" : "2.2.3" + } + }, + { + "identity" : "swift-perception", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-perception", + "state" : { + "revision" : "9e9ec5177724e78de1ecfd5b7bf881ae027be4ac", + "version" : "1.4.2" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + }, + { + "identity" : "swift-tagged", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-tagged.git", + "state" : { + "revision" : "3907a9438f5b57d317001dc99f3f11b46882272b", + "version" : "0.10.0" + } + }, + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/swiftui-introspect", + "state" : { + "revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336", + "version" : "1.3.0" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1", + "version" : "1.4.3" + } + } + ], + "version" : 2 +} diff --git a/Tuist/Package.swift b/Tuist/Package.swift new file mode 100644 index 00000000..fbabcf5f --- /dev/null +++ b/Tuist/Package.swift @@ -0,0 +1,44 @@ +// swift-tools-version: 6.0 +import PackageDescription + +#if TUIST + import ProjectDescription + + let packageSettings = PackageSettings( + // Customize the product types for specific package product + // Default is .staticFramework + // productTypes: ["Alamofire": .framework,] + productTypes: [ + "ConcurrencyExtras": .framework, + "XCTestDynamicOverlay": .framework, + "CombineSchedulers": .framework, + "Dependencies": .framework, + "Clocks": .framework, + "Tagged": .framework, + "SwiftUINavigation": .framework, + "PopupView": .framework, + "IssueReporting": .framework, + "ComposableArchitecture": .framework, + "PerceptionCore": .framework, + "Perception": .framework, + "OrderedCollections": .framework, + "CasePaths": .framework, + "CustomDump": .framework, + "SwiftNavigation": .framework, + "CasePathsCore": .framework, + "InternalCollectionsUtilities": .framework, + "UIKitNavigation": .framework, + "UIKitNavigationShim": .framework + ] + ) +#endif + +let package = Package( + name: "ModernCleanArchitecture", + dependencies: [ + .package(url: "https://github.com/apollographql/apollo-ios.git", exact: "1.12.2"), + .package(url: "https://github.com/pointfreeco/swift-tagged.git", exact: "0.10.0"), + .package(url: "https://github.com/exyte/PopupView.git", exact: "4.0.0"), + .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git", exact: "1.13.1") + ] +) diff --git a/Tuist/ProjectDescriptionHelpers/Helpers/Array+Extensions.swift b/Tuist/ProjectDescriptionHelpers/Helpers/Array+Extensions.swift new file mode 100644 index 00000000..53bfd0a7 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Helpers/Array+Extensions.swift @@ -0,0 +1,24 @@ + +extension Array { + static func build(@ArrayBuilder _ builder: () -> [Element]) -> [Element] { + Array(builder()) + } +} + +@resultBuilder +public struct ArrayBuilder { + public static func buildPartialBlock(first: Element) -> [Element] { [first] } + public static func buildPartialBlock(first: [Element]) -> [Element] { first } + public static func buildPartialBlock(accumulated: [Element], next: Element) -> [Element] { accumulated + [next] } + public static func buildPartialBlock(accumulated: [Element], next: [Element]) -> [Element] { accumulated + next } + + // Empty Case + public static func buildBlock() -> [Element] { [] } + // If/Else + public static func buildEither(first: [Element]) -> [Element] { first } + public static func buildEither(second: [Element]) -> [Element] { second } + // Just ifs + public static func buildIf(_ element: [Element]?) -> [Element] { element ?? [] } + // fatalError() + public static func buildPartialBlock(first: Never) -> [Element] {} +} diff --git a/Tuist/ProjectDescriptionHelpers/ProjectName.swift b/Tuist/ProjectDescriptionHelpers/ProjectName.swift new file mode 100644 index 00000000..f7d4085e --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/ProjectName.swift @@ -0,0 +1,53 @@ +import ProjectDescription + +public enum ProjectName: String, CaseIterable { + case Core + case Domain + case Example + case Features + case Platform + case UI +} + +public extension ProjectName { + var projectPath: Path { + "Projects/\(rawValue)" + } +} + +public extension ProjectName { + var project: Project { + switch self { + case .Core: + Project( + name: rawValue, + targets: CoreModuleName.allCases.map(\.target) + ) + case .Domain: + Project( + name: rawValue, + targets: DomainModuleName.allCases.map(\.target) + ) + case .Example: + Project( + name: rawValue, + targets: ExampleModuleName.allCases.map(\.target) + ) + case .Features: + Project( + name: rawValue, + targets: FeaturesModuleName.allCases.map(\.target) + ) + case .Platform: + Project( + name: rawValue, + targets: PlatformModuleName.allCases.map(\.target) + ) + case .UI: + Project( + name: rawValue, + targets: UIModuleName.allCases.map(\.target) + ) + } + } +} diff --git a/Tuist/ProjectDescriptionHelpers/Projects/CoreModuleName.swift b/Tuist/ProjectDescriptionHelpers/Projects/CoreModuleName.swift new file mode 100644 index 00000000..ba078eab --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Projects/CoreModuleName.swift @@ -0,0 +1,63 @@ +import ProjectDescription + +enum CoreModuleName: String, CaseIterable { + case FileCache + case HTTPClient + case ApolloExtensions + case SwiftDataHelpers +} + +extension CoreModuleName { + var target: Target { + switch self { + case .FileCache: + .target( + name: rawValue, + destinations: .iOS, + product: .framework, + bundleId: "com.sergdort.\(rawValue)", + sources: "\(rawValue)/**" + ) + case .HTTPClient: + .target( + name: rawValue, + destinations: .iOS, + product: .framework, + bundleId: "com.sergdort.\(rawValue)", + sources: "\(rawValue)/**" + ) + case .ApolloExtensions: + .target( + name: rawValue, + destinations: .iOS, + product: .framework, + bundleId: "com.sergdort.\(rawValue)", + sources: "\(rawValue)/**", + dependencies: [ + .external(.Apollo) + ] + ) + case .SwiftDataHelpers: + .target( + name: rawValue, + destinations: .iOS, + product: .framework, + bundleId: "com.sergdort.\(rawValue)", + sources: "\(rawValue)/**", + dependencies: [ + .external(.Dependencies), + .external(.XCTestDynamicOverlay) + ] + ) + } + } +} + +extension TargetDependency { + static func fromCore(_ name: CoreModuleName) -> Self { + .project( + target: name.rawValue, + path: .path("../Core") + ) + } +} diff --git a/Tuist/ProjectDescriptionHelpers/Projects/DomainModuleName.swift b/Tuist/ProjectDescriptionHelpers/Projects/DomainModuleName.swift new file mode 100644 index 00000000..d47a1696 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Projects/DomainModuleName.swift @@ -0,0 +1,46 @@ +import ProjectDescription + +enum DomainModuleName: String, CaseIterable { + case MoviesDomain + case AnimeDomain +} + +extension DomainModuleName { + var target: Target { + .domain( + name: self, + external: [ + .Dependencies, + .XCTestDynamicOverlay, + .Tagged + ] + ) + } +} + +extension Target { + static func domain( + name: DomainModuleName, + external: [ExternalDependenciesName] + ) -> Target { + .target( + name: name.rawValue, + destinations: .iOS, + product: .framework, + bundleId: "com.sergdort.\(name.rawValue)", + sources: ["\(name.rawValue)/**"], + dependencies: .build { + external.map(TargetDependency.external) + } + ) + } +} + +extension TargetDependency { + static func fromDomain(_ name: DomainModuleName) -> Self { + .project( + target: name.rawValue, + path: .path("../Domain") + ) + } +} diff --git a/Tuist/ProjectDescriptionHelpers/Projects/ExampleModuleName.swift b/Tuist/ProjectDescriptionHelpers/Projects/ExampleModuleName.swift new file mode 100644 index 00000000..83b7ee0d --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Projects/ExampleModuleName.swift @@ -0,0 +1,32 @@ +import ProjectDescription + +enum ExampleModuleName: String, CaseIterable { + case Example +} + +extension ExampleModuleName { + var target: Target { + switch self { + case .Example: + return .target( + name: rawValue, + destinations: [.iPhone], + product: .app, + bundleId: "com.sergdort.\(rawValue)", + infoPlist: .file(path: .relativeToManifest("Info.plist")), + sources: ["Sources/**"], + resources: ["Resources/**"], + dependencies: .build { + FeaturesModuleName.allCases.map(TargetDependency.fromFeatures) + PlatformModuleName.allCases.map(TargetDependency.fromPlatfrom) + DomainModuleName.allCases.map(TargetDependency.fromDomain) + CoreModuleName.allCases.map(TargetDependency.fromCore) + UIModuleName.allCases.map(TargetDependency.fromUI) + + TargetDependency.external(.Dependencies) + TargetDependency.external(.SwiftUINavigation) + } + ) + } + } +} diff --git a/Tuist/ProjectDescriptionHelpers/Projects/ExternalDependenciesName.swift b/Tuist/ProjectDescriptionHelpers/Projects/ExternalDependenciesName.swift new file mode 100644 index 00000000..e5a08780 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Projects/ExternalDependenciesName.swift @@ -0,0 +1,20 @@ +import ProjectDescription + +enum ExternalDependenciesName: String { + case Apollo + case Dependencies + case XCTestDynamicOverlay + case Tagged + case Clocks + case ConcurrencyExtras + case CombineSchedulers + case SwiftUINavigation + case PopupView + case ComposableArchitecture +} + +extension TargetDependency { + static func external(_ name: ExternalDependenciesName) -> Self { + .external(name: name.rawValue) + } +} diff --git a/Tuist/ProjectDescriptionHelpers/Projects/FeaturesModuleName.swift b/Tuist/ProjectDescriptionHelpers/Projects/FeaturesModuleName.swift new file mode 100644 index 00000000..76363f66 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Projects/FeaturesModuleName.swift @@ -0,0 +1,73 @@ +import ProjectDescription + +enum FeaturesModuleName: String, CaseIterable { + case Movies + case Anime + case Watchlist +} + +extension FeaturesModuleName { + var target: Target { + switch self { + case .Movies: + return .feature( + self, + domains: [ + .MoviesDomain + ], + external: [ + .Dependencies + ] + ) + case .Anime: + return .feature( + self, + domains: [ + .AnimeDomain + ], + external: [ + .Dependencies, + .ComposableArchitecture + ] + ) + case .Watchlist: + return .feature( + self, + domains: [ + .AnimeDomain, + .MoviesDomain + ], + external: [ + .Dependencies + ] + ) + } + } +} + +extension Target { + static func feature( + _ name: FeaturesModuleName, + domains: [DomainModuleName], + external: [ExternalDependenciesName] = [] + ) -> Self { + .target( + name: name.rawValue, + destinations: .iOS, + product: .framework, + bundleId: "com.sergdort.\(name.rawValue)", + sources: ["\(name.rawValue)/**"], + dependencies: .build { + domains.map(TargetDependency.fromDomain) + TargetDependency.fromUI(.UI) + external.map(TargetDependency.external) + } + ) + } +} + +extension TargetDependency { + static func fromFeatures(_ name: FeaturesModuleName) -> Self { + .project(target: name.rawValue, path: .path("../Features")) + } +} diff --git a/Tuist/ProjectDescriptionHelpers/Projects/PlatformModuleName.swift b/Tuist/ProjectDescriptionHelpers/Projects/PlatformModuleName.swift new file mode 100644 index 00000000..d63a1f44 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Projects/PlatformModuleName.swift @@ -0,0 +1,93 @@ +import ProjectDescription +import Foundation + +enum PlatformModuleName: String, CaseIterable { + case MoviesAPI + case MoviesDB + case AnimeAPI + case AnimeDB +} + +extension PlatformModuleName { + var target: Target { + switch self { + case .MoviesAPI: + return .platform( + self, + domains: [ + .MoviesDomain + ], + coreDependencies: [ + .HTTPClient + ] + ) + case .MoviesDB: + return .platform( + self, + domains: [ + .MoviesDomain + ], + coreDependencies: [ + .FileCache, + .SwiftDataHelpers + ] + ) + case .AnimeAPI: + return .platform( + self, + domains: [ + .AnimeDomain + ], + coreDependencies: [ + .ApolloExtensions + ], + additionalFiles: [ + .glob( + pattern: "\(self.rawValue)/**/*.graphql" + ) + ] + ) + case .AnimeDB: + return .platform( + self, + domains: [ + .AnimeDomain + ], + coreDependencies: [ + .SwiftDataHelpers + ] + ) + } + } +} + +extension Target { + static func platform( + _ name: PlatformModuleName, + domains: [DomainModuleName] = [], + coreDependencies: [CoreModuleName] = [], + externalDependencies: [ExternalDependenciesName] = [], + additionalFiles: [FileElement] = [] + ) -> Self { + return .target( + name: name.rawValue, + destinations: .iOS, + product: .framework, + bundleId: "com.sergdort.\(name.rawValue)", + sources: ["\(name.rawValue)/**"], + dependencies: .build { + domains.map(TargetDependency.fromDomain) + coreDependencies.map(TargetDependency.fromCore) + externalDependencies.map(TargetDependency.external) + }, + additionalFiles: additionalFiles + ) + } +} + +extension TargetDependency { + static func fromPlatfrom(_ name: PlatformModuleName) -> Self { + .project(target: name.rawValue, path: .path("../Platform")) + } +} + diff --git a/Tuist/ProjectDescriptionHelpers/Projects/UIModuleName.swift b/Tuist/ProjectDescriptionHelpers/Projects/UIModuleName.swift new file mode 100644 index 00000000..87ad4218 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Projects/UIModuleName.swift @@ -0,0 +1,28 @@ +import ProjectDescription + +enum UIModuleName: String, CaseIterable { + case UI +} + +extension UIModuleName { + var target: Target { + .target( + name: rawValue, + destinations: .iOS, + product: .framework, + bundleId: "com.sergdort.\(rawValue)", + sources: ["\(rawValue)/**"], + dependencies: [ + .external(.PopupView), + .external(.Dependencies), + .external(.ComposableArchitecture) + ] + ) + } +} + +extension TargetDependency { + static func fromUI(_ name: UIModuleName) -> Self { + .project(target: name.rawValue, path: .path("../UI")) + } +} diff --git a/Workspace.swift b/Workspace.swift new file mode 100644 index 00000000..efd1a838 --- /dev/null +++ b/Workspace.swift @@ -0,0 +1,7 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let workspace = Workspace( + name: "ModernCleanArchtecture", + projects: ProjectName.allCases.map(\.projectPath) +) diff --git a/apollo-codegen-config.json b/apollo-codegen-config.json new file mode 100644 index 00000000..4ecd6155 --- /dev/null +++ b/apollo-codegen-config.json @@ -0,0 +1,28 @@ +{ + "schemaNamespace" : "AnimeAPI", + "input" : { + "operationSearchPaths" : [ + "**/*.graphql" + ], + "schemaSearchPaths" : [ + "**/*.graphqls" + ] + }, + "output" : { + "testMocks" : { + "none" : { + } + }, + "schemaTypes" : { + "path" : "./Projects/Platform/AnimeAPI/GraphQL/Generated", + "moduleType" : { + "other" : { + } + } + }, + "operations" : { + "inSchemaModule" : { + } + } + } +} \ No newline at end of file diff --git a/apollo-ios-cli b/apollo-ios-cli new file mode 100755 index 00000000..603300c3 Binary files /dev/null and b/apollo-ios-cli differ diff --git a/Architecture/MVVMPattern.png b/images/MVVMPattern.png similarity index 100% rename from Architecture/MVVMPattern.png rename to images/MVVMPattern.png diff --git a/images/anime_detail.gif b/images/anime_detail.gif new file mode 100644 index 00000000..90cf2ffb Binary files /dev/null and b/images/anime_detail.gif differ diff --git a/images/anime_list.gif b/images/anime_list.gif new file mode 100644 index 00000000..72d3a453 Binary files /dev/null and b/images/anime_list.gif differ diff --git a/images/app_layers.png b/images/app_layers.png new file mode 100644 index 00000000..89100091 Binary files /dev/null and b/images/app_layers.png differ diff --git a/images/feature_dependencies.png b/images/feature_dependencies.png new file mode 100644 index 00000000..b591cc1a Binary files /dev/null and b/images/feature_dependencies.png differ diff --git a/images/graph.png b/images/graph.png new file mode 100644 index 00000000..52beb715 Binary files /dev/null and b/images/graph.png differ diff --git a/images/layered_architecture.png b/images/layered_architecture.png new file mode 100644 index 00000000..a8aa4116 Binary files /dev/null and b/images/layered_architecture.png differ diff --git a/images/layers.drawing b/images/layers.drawing new file mode 100644 index 00000000..1ad1b10a Binary files /dev/null and b/images/layers.drawing differ diff --git a/images/layers_responsibility.png b/images/layers_responsibility.png new file mode 100644 index 00000000..28ea67bf Binary files /dev/null and b/images/layers_responsibility.png differ diff --git a/images/movie_detail.gif b/images/movie_detail.gif new file mode 100644 index 00000000..8be1dbb8 Binary files /dev/null and b/images/movie_detail.gif differ diff --git a/images/movies_list.gif b/images/movies_list.gif new file mode 100644 index 00000000..5c9f5247 Binary files /dev/null and b/images/movies_list.gif differ diff --git a/images/mylists.gif b/images/mylists.gif new file mode 100644 index 00000000..38285286 Binary files /dev/null and b/images/mylists.gif differ diff --git a/images/platform_dependecies.png b/images/platform_dependecies.png new file mode 100644 index 00000000..40e9c2b7 Binary files /dev/null and b/images/platform_dependecies.png differ diff --git a/images/plugin_architecture.png b/images/plugin_architecture.png new file mode 100644 index 00000000..302dc830 Binary files /dev/null and b/images/plugin_architecture.png differ