diff --git a/CleanArchitectureRxSwift.xcodeproj/project.pbxproj b/CleanArchitectureRxSwift.xcodeproj/project.pbxproj index 400dbdbb..74fe290b 100644 --- a/CleanArchitectureRxSwift.xcodeproj/project.pbxproj +++ b/CleanArchitectureRxSwift.xcodeproj/project.pbxproj @@ -87,6 +87,7 @@ 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 */; }; BC8D07731E9309D000B4D96A /* UidTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC8D07721E9309D000B4D96A /* UidTransform.swift */; }; BCD8C8AC1E73421300F79E3E /* Address+Mapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD8C8AB1E73421300F79E3E /* Address+Mapping.swift */; }; BCD8C8AE1E73421E00F79E3E /* Album+Mapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD8C8AD1E73421E00F79E3E /* Album+Mapping.swift */; }; @@ -375,6 +376,7 @@ 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 = ""; }; BC8D07721E9309D000B4D96A /* UidTransform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UidTransform.swift; sourceTree = ""; }; @@ -569,13 +571,6 @@ path = Encodable; sourceTree = ""; }; - 25707C721F23807C00F852F7 /* Repository */ = { - isa = PBXGroup; - children = ( - ); - name = Repository; - sourceTree = ""; - }; 25707C731F2380E500F852F7 /* Cache */ = { isa = PBXGroup; children = ( @@ -892,6 +887,7 @@ 515F9401ED57113EEF898228 /* EditPost */ = { isa = PBXGroup; children = ( + B60A97CD1FB0C17600009C51 /* EditPostNavigator.swift */, 2526A7151E5A2CD30078870E /* EditPostViewController.swift */, 515F9ACD2D474324015B691C /* EditPostViewModel.swift */, ); @@ -1033,7 +1029,6 @@ isa = PBXGroup; children = ( 25707C731F2380E500F852F7 /* Cache */, - 25707C721F23807C00F852F7 /* Repository */, BD107F671E72A0DC0043D900 /* API */, BD107F731E72B1790043D900 /* Entries */, BD50EEF31E7AD99400CBEBD4 /* Network */, @@ -2031,6 +2026,7 @@ 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 */, diff --git a/CleanArchitectureRxSwift/Base.lproj/Main.storyboard b/CleanArchitectureRxSwift/Base.lproj/Main.storyboard index 8bb57ecf..7bb25b97 100644 --- a/CleanArchitectureRxSwift/Base.lproj/Main.storyboard +++ b/CleanArchitectureRxSwift/Base.lproj/Main.storyboard @@ -1,12 +1,13 @@ - + - + + @@ -30,7 +31,7 @@ - + @@ -224,9 +225,13 @@ - + + + + + diff --git a/CleanArchitectureRxSwift/Scenes/AllPosts/PostsNavigator.swift b/CleanArchitectureRxSwift/Scenes/AllPosts/PostsNavigator.swift index 87da9e96..c5fcbee3 100644 --- a/CleanArchitectureRxSwift/Scenes/AllPosts/PostsNavigator.swift +++ b/CleanArchitectureRxSwift/Scenes/AllPosts/PostsNavigator.swift @@ -36,10 +36,11 @@ class DefaultPostsNavigator: PostsNavigator { 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) - let viewModel = EditPostViewModel(post: post, useCase: services.makePostsUseCase()) vc.viewModel = viewModel navigationController.pushViewController(vc, animated: true) } diff --git a/CleanArchitectureRxSwift/Scenes/CreatePost/CreatePostViewModel.swift b/CleanArchitectureRxSwift/Scenes/CreatePost/CreatePostViewModel.swift index dc7462f4..b131ce48 100644 --- a/CleanArchitectureRxSwift/Scenes/CreatePost/CreatePostViewModel.swift +++ b/CleanArchitectureRxSwift/Scenes/CreatePost/CreatePostViewModel.swift @@ -28,10 +28,9 @@ final class CreatePostViewModel: ViewModelType { return !$0.0.isEmpty && !$0.1.isEmpty && !$1 } - let save = input.saveTrigger.withLatestFrom(titleAndDetails) .map { (title, content) in - return Post(body: content, title: title, uid: "5", userId: "7") + return Post(body: content, title: title) } .flatMapLatest { [unowned self] in return self.createPostUseCase.save(post: $0) diff --git a/CleanArchitectureRxSwift/Scenes/EditPost/EditPostNavigator.swift b/CleanArchitectureRxSwift/Scenes/EditPost/EditPostNavigator.swift new file mode 100644 index 00000000..4b6f4db8 --- /dev/null +++ b/CleanArchitectureRxSwift/Scenes/EditPost/EditPostNavigator.swift @@ -0,0 +1,19 @@ +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 index 62fa9007..2f1ffcff 100644 --- a/CleanArchitectureRxSwift/Scenes/EditPost/EditPostViewController.swift +++ b/CleanArchitectureRxSwift/Scenes/EditPost/EditPostViewController.swift @@ -7,6 +7,7 @@ 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! @@ -15,20 +16,44 @@ final class EditPostViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - let input = EditPostViewModel.Input(editTrigger: editButton.rx.tap.asDriver(), - title: titleTextField.rx.text.orEmpty.asDriver(), - details: detailsTextView.rx.text.orEmpty.asDriver()) + 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 + ) + + [("Yes", UIAlertActionStyle.destructive, { _ -> () in observer.onNext() }), + ("No", UIAlertActionStyle.cancel, { _ -> () in observer.onCompleted() })] + .map({ UIAlertAction(title: $0, style: $1, handler: $2) }) + .forEach(alert.addAction) + + 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).addDisposableTo(disposeBag) - output.editing.drive(titleTextField.rx.isEnabled).addDisposableTo(disposeBag) - output.editing.drive(detailsTextView.rx.isEditable).addDisposableTo(disposeBag) - output.post.drive(postBinding).addDisposableTo(disposeBag) - output.save.drive().addDisposableTo(disposeBag) - output.error.drive(errorBinding).addDisposableTo(disposeBag) + + [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.addDisposableTo(disposeBag)}) } - var postBinding: UIBindingObserver { return UIBindingObserver(UIElement: self, binding: { (vc, post) in vc.titleTextField.text = post.title @@ -60,4 +85,3 @@ extension Reactive where Base: UITextView { }) } } - diff --git a/CleanArchitectureRxSwift/Scenes/EditPost/EditPostViewModel.swift b/CleanArchitectureRxSwift/Scenes/EditPost/EditPostViewModel.swift index ebbc71f5..e749f1de 100644 --- a/CleanArchitectureRxSwift/Scenes/EditPost/EditPostViewModel.swift +++ b/CleanArchitectureRxSwift/Scenes/EditPost/EditPostViewModel.swift @@ -5,10 +5,12 @@ import RxCocoa final class EditPostViewModel: ViewModelType { private let post: Post private let useCase: PostsUseCase + private let navigator: EditPostNavigator - init(post: Post, useCase: PostsUseCase) { + init(post: Post, useCase: PostsUseCase, navigator: EditPostNavigator) { self.post = post self.useCase = useCase + self.navigator = navigator } func transform(input: Input) -> Output { @@ -24,7 +26,7 @@ final class EditPostViewModel: ViewModelType { $0 } let post = Driver.combineLatest(Driver.just(self.post), titleAndDetails) { (post, titleAndDetails) -> Post in - return Post(body: titleAndDetails.1, title: titleAndDetails.0, uid: "7", userId: "8") + 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" @@ -35,8 +37,19 @@ final class EditPostViewModel: ViewModelType { .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()) @@ -46,6 +59,7 @@ final class EditPostViewModel: ViewModelType { extension EditPostViewModel { struct Input { let editTrigger: Driver + let deleteTrigger: Driver let title: Driver let details: Driver } @@ -53,6 +67,7 @@ extension EditPostViewModel { struct Output { let editButtonTitle: Driver let save: Driver + let delete: Driver let editing: Driver let post: Driver let error: Driver diff --git a/CoreDataPlatform/Entities/CDPost+CoreDataProperties.swift b/CoreDataPlatform/Entities/CDPost+CoreDataProperties.swift index 3926e561..309565cb 100644 --- a/CoreDataPlatform/Entities/CDPost+CoreDataProperties.swift +++ b/CoreDataPlatform/Entities/CDPost+CoreDataProperties.swift @@ -20,5 +20,5 @@ extension CDPost { @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 index c98d3d36..587d6b64 100644 --- a/CoreDataPlatform/Entities/CDPost+Ext.swift +++ b/CoreDataPlatform/Entities/CDPost+Ext.swift @@ -18,6 +18,7 @@ extension CDPost { 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 { @@ -25,7 +26,8 @@ extension CDPost: DomainConvertibleType { return Post(body: body!, title: title!, uid: uid!, - userId: userId!) + userId: userId!, + createdAt: createdAt!) } } @@ -43,5 +45,6 @@ extension Post: CoreDataRepresentable { entity.title = title entity.body = body entity.userId = userId + entity.createdAt = createdAt } } diff --git a/CoreDataPlatform/Model.xcdatamodeld/Model.xcdatamodel/contents b/CoreDataPlatform/Model.xcdatamodeld/Model.xcdatamodel/contents index 59539159..1d2cd624 100644 --- a/CoreDataPlatform/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/CoreDataPlatform/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -40,6 +40,7 @@ + @@ -67,7 +68,7 @@ - + diff --git a/CoreDataPlatform/Repository/Repository.swift b/CoreDataPlatform/Repository/Repository.swift index 3a897d7a..3c90aea0 100644 --- a/CoreDataPlatform/Repository/Repository.swift +++ b/CoreDataPlatform/Repository/Repository.swift @@ -1,22 +1,17 @@ import Foundation import CoreData import RxSwift +import QueryKit -func abstractMethod() -> Never { - fatalError("abstract method") +protocol AbstractRepository { + associatedtype T + func query(with predicate: NSPredicate?, + sortDescriptors: [NSSortDescriptor]?) -> Observable<[T]> + func save(entity: T) -> Observable + func delete(entity: T) -> Observable } -class AbstractRepository { - func query(with predicate: NSPredicate? = nil, - sortDescriptors: [NSSortDescriptor]? = nil) -> Observable<[T]> { - abstractMethod() - } - func save(entity: T) -> Observable { - abstractMethod() - } -} - -final class Repository: AbstractRepository where T == T.CoreDataType.DomainType { +final class Repository: AbstractRepository where T == T.CoreDataType.DomainType { private let context: NSManagedObjectContext private let scheduler: ContextScheduler @@ -25,7 +20,7 @@ final class Repository: AbstractRepository where T self.scheduler = ContextScheduler(context: context) } - override func query(with predicate: NSPredicate? = nil, + func query(with predicate: NSPredicate? = nil, sortDescriptors: [NSSortDescriptor]? = nil) -> Observable<[T]> { let request = T.CoreDataType.fetchRequest() request.predicate = predicate @@ -35,10 +30,17 @@ final class Repository: AbstractRepository where T .subscribeOn(scheduler) } - override func save(entity: T) -> Observable { + 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/NSManagedObjectContext+Rx.swift b/CoreDataPlatform/RxCoreData/NSManagedObjectContext+Rx.swift index 001000ae..7457d5eb 100644 --- a/CoreDataPlatform/RxCoreData/NSManagedObjectContext+Rx.swift +++ b/CoreDataPlatform/RxCoreData/NSManagedObjectContext+Rx.swift @@ -37,6 +37,16 @@ extension Reactive where Base: NSManagedObjectContext { } } + 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) diff --git a/CoreDataPlatform/UseCases/PostsUseCase.swift b/CoreDataPlatform/UseCases/PostsUseCase.swift index ae0f61ac..354b2fae 100644 --- a/CoreDataPlatform/UseCases/PostsUseCase.swift +++ b/CoreDataPlatform/UseCases/PostsUseCase.swift @@ -2,19 +2,23 @@ import Foundation import Domain import RxSwift -final class PostsUseCase: Domain.PostsUseCase { +final class PostsUseCase: Domain.PostsUseCase where Repository: AbstractRepository, Repository.T == Post { - private let repository: AbstractRepository + private let repository: Repository - init(repository: AbstractRepository) { + init(repository: Repository) { self.repository = repository } func posts() -> Observable<[Post]> { - return repository.query(sortDescriptors: [Post.CoreDataType.uid.descending()]) + 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/Domain/Entries/Post.swift b/Domain/Entries/Post.swift index ddde3041..ec012c78 100644 --- a/Domain/Entries/Post.swift +++ b/Domain/Entries/Post.swift @@ -5,15 +5,22 @@ public struct Post { 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) { + 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))) } } @@ -22,6 +29,7 @@ extension Post: Equatable { return lhs.uid == rhs.uid && lhs.title == rhs.title && lhs.body == rhs.body && - lhs.userId == rhs.userId + lhs.userId == rhs.userId && + lhs.createdAt == rhs.createdAt } } diff --git a/Domain/UseCases/PostsUseCase.swift b/Domain/UseCases/PostsUseCase.swift index adfff402..12051264 100644 --- a/Domain/UseCases/PostsUseCase.swift +++ b/Domain/UseCases/PostsUseCase.swift @@ -4,4 +4,5 @@ import RxSwift public protocol PostsUseCase { func posts() -> Observable<[Post]> func save(post: Post) -> Observable + func delete(post: Post) -> Observable } diff --git a/NetworkPlatform/Cache/Cache.swift b/NetworkPlatform/Cache/Cache.swift index 5c4f49ec..3914393b 100644 --- a/NetworkPlatform/Cache/Cache.swift +++ b/NetworkPlatform/Cache/Cache.swift @@ -1,28 +1,15 @@ import Foundation import RxSwift -func abstractMethod() -> Never { - fatalError("abstract method") +protocol AbstractCache { + associatedtype T + func save(object: T) -> Completable + func save(objects: [T]) -> Completable + func fetch(withID id: String) -> Maybe + func fetchObjects() -> Maybe<[T]> } -class AbstractCache { - func save(object: T) -> Completable { - abstractMethod() - } - func save(objects: [T]) -> Completable { - abstractMethod() - } - - func fetch(withID id: String) -> Maybe { - abstractMethod() - } - - func fetchObjects() -> Maybe<[T]> { - abstractMethod() - } -} - -final class Cache: AbstractCache where T == T.Encoder.DomainType { +final class Cache: AbstractCache where T == T.Encoder.DomainType { enum Error: Swift.Error { case saveObject(T) case saveObjects([T]) @@ -45,7 +32,7 @@ final class Cache: AbstractCache where T == T.Encoder.DomainTyp self.path = path } - override func save(object: T) -> Completable { + func save(object: T) -> Completable { return Completable.create { (observer) -> Disposable in guard let url = FileManager.default .urls(for: .documentDirectory, in: .userDomainMask).first else { @@ -67,7 +54,7 @@ final class Cache: AbstractCache where T == T.Encoder.DomainTyp }.subscribeOn(cacheScheduler) } - override func save(objects: [T]) -> Completable { + func save(objects: [T]) -> Completable { return Completable.create { (observer) -> Disposable in guard let directoryURL = self.directoryURL() else { observer(.completed) @@ -88,7 +75,7 @@ final class Cache: AbstractCache where T == T.Encoder.DomainTyp }.subscribeOn(cacheScheduler) } - override func fetch(withID id: String) -> Maybe { + func fetch(withID id: String) -> Maybe { return Maybe.create { (observer) -> Disposable in guard let url = FileManager.default .urls(for: .documentDirectory, in: .userDomainMask).first else { @@ -109,7 +96,7 @@ final class Cache: AbstractCache where T == T.Encoder.DomainTyp }.subscribeOn(cacheScheduler) } - override func fetchObjects() -> Maybe<[T]> { + func fetchObjects() -> Maybe<[T]> { return Maybe<[T]>.create { (observer) -> Disposable in guard let directoryURL = self.directoryURL() else { observer(.completed) diff --git a/NetworkPlatform/Entries/Post+Mapping.swift b/NetworkPlatform/Entries/Post+Mapping.swift index 653c6a0c..073b86e3 100644 --- a/NetworkPlatform/Entries/Post+Mapping.swift +++ b/NetworkPlatform/Entries/Post+Mapping.swift @@ -17,6 +17,7 @@ extension Post: ImmutableMappable, Identifiable { title = try map.value("title") uid = try map.value("id", using: UidTransform()) userId = try map.value("userId", using: UidTransform()) + createdAt = (try? map.value("createdAt", using: UidTransform())) ?? "" } } @@ -32,17 +33,20 @@ final class NETPost: NSObject, NSCoding, DomainConvertibleType { 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) { @@ -50,7 +54,8 @@ final class NETPost: NSObject, NSCoding, DomainConvertibleType { 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 userId = aDecoder.decodeObject(forKey: Keys.userId) as? String, + let createdAt = aDecoder.decodeObject(forKey: Keys.createdAt) as? String else { return nil } @@ -58,6 +63,7 @@ final class NETPost: NSObject, NSCoding, DomainConvertibleType { self.title = title self.uid = uid self.userId = userId + self.createdAt = createdAt } func encode(with aCoder: NSCoder) { @@ -65,12 +71,14 @@ final class NETPost: NSObject, NSCoding, DomainConvertibleType { 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) + userId: userId, + createdAt: createdAt) } } diff --git a/NetworkPlatform/UseCases/PostsUseCase.swift b/NetworkPlatform/UseCases/PostsUseCase.swift index 9313f8c2..87317d8d 100644 --- a/NetworkPlatform/UseCases/PostsUseCase.swift +++ b/NetworkPlatform/UseCases/PostsUseCase.swift @@ -2,11 +2,11 @@ import Foundation import Domain import RxSwift -final class PostsUseCase: Domain.PostsUseCase { +final class PostsUseCase: Domain.PostsUseCase where Cache: AbstractCache, Cache.T == Post { private let network: PostsNetwork - private let cache: AbstractCache + private let cache: Cache - init(network: PostsNetwork, cache: AbstractCache) { + init(network: PostsNetwork, cache: Cache) { self.network = network self.cache = cache } @@ -28,6 +28,10 @@ final class PostsUseCase: Domain.PostsUseCase { return network.createPost(post: post) .map { _ in } } + + func delete(post: Post) -> Observable { + return network.deletePost(postId: post.uid).map({_ in}) + } } struct MapFromNever: Error {} diff --git a/README.md b/README.md index a7224aef..e31641a7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Clean architecture with [RxSwift](https://github.com/ReactiveX/RxSwift) -## Contibutions are welcome and highly appreciated!! +## 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 😊) diff --git a/RealmPlatform/Entities/RMPost.swift b/RealmPlatform/Entities/RMPost.swift index b1c0aa9c..b49b4e82 100644 --- a/RealmPlatform/Entities/RMPost.swift +++ b/RealmPlatform/Entities/RMPost.swift @@ -8,6 +8,7 @@ final class RMPost: Object { dynamic var userId: String = "" dynamic var title: String = "" dynamic var body: String = "" + dynamic var createdAt: String = "" override class func primaryKey() -> String? { return "uid" @@ -19,6 +20,7 @@ extension RMPost { 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 { @@ -26,7 +28,8 @@ extension RMPost: DomainConvertibleType { return Post(body: body, title: title, uid: uid, - userId: userId) + userId: userId, + createdAt: createdAt) } } @@ -37,6 +40,7 @@ extension Post: RealmRepresentable { object.userId = userId object.title = title object.body = body + object.createdAt = createdAt } } } diff --git a/RealmPlatform/Repository/Repository.swift b/RealmPlatform/Repository/Repository.swift index 1ef954a4..0f914725 100644 --- a/RealmPlatform/Repository/Repository.swift +++ b/RealmPlatform/Repository/Repository.swift @@ -4,31 +4,16 @@ import RealmSwift import RxSwift import RxRealm -func abstractMethod() -> Never { - fatalError("abstract method") -} - -class AbstractRepository { - - func queryAll() -> Observable<[T]> { - abstractMethod() - } - +protocol AbstractRepository { + associatedtype T + func queryAll() -> Observable<[T]> func query(with predicate: NSPredicate, - sortDescriptors: [NSSortDescriptor] = []) -> Observable<[T]> { - abstractMethod() - } - - func save(entity: T) -> Observable { - abstractMethod() - } - - func delete(entity: T) -> Observable { - abstractMethod() - } + 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 { +final class Repository: AbstractRepository where T == T.RealmType.DomainType, T.RealmType: Object { private let configuration: Realm.Configuration private let scheduler: RunLoopThreadScheduler @@ -43,7 +28,7 @@ final class Repository: AbstractRepository where T == T print("File 📁 url: \(RLMRealmPathForFile("default.realm"))") } - override func queryAll() -> Observable<[T]> { + func queryAll() -> Observable<[T]> { return Observable.deferred { let realm = self.realm let objects = realm.objects(T.RealmType.self) @@ -54,7 +39,7 @@ final class Repository: AbstractRepository where T == T .subscribeOn(scheduler) } - override func query(with predicate: NSPredicate, + func query(with predicate: NSPredicate, sortDescriptors: [NSSortDescriptor] = []) -> Observable<[T]> { return Observable.deferred { let realm = self.realm @@ -70,18 +55,16 @@ final class Repository: AbstractRepository where T == T .subscribeOn(scheduler) } - override func save(entity: T) -> Observable { + func save(entity: T) -> Observable { return Observable.deferred { - let realm = self.realm - return realm.rx.save(entity: entity) + return self.realm.rx.save(entity: entity) }.subscribeOn(scheduler) } - override func delete(entity: T) -> Observable { + func delete(entity: T) -> Observable { return Observable.deferred { return self.realm.rx.delete(entity: entity) - } - .subscribeOn(scheduler) + }.subscribeOn(scheduler) } } diff --git a/RealmPlatform/UseCases/PostsUseCase.swift b/RealmPlatform/UseCases/PostsUseCase.swift index 83fecc98..5a00f53f 100644 --- a/RealmPlatform/UseCases/PostsUseCase.swift +++ b/RealmPlatform/UseCases/PostsUseCase.swift @@ -4,10 +4,11 @@ import RxSwift import Realm import RealmSwift -final class PostsUseCase: Domain.PostsUseCase { - private let repository: AbstractRepository +final class PostsUseCase: Domain.PostsUseCase where Repository: AbstractRepository, Repository.T == Post { - init(repository: AbstractRepository) { + private let repository: Repository + + init(repository: Repository) { self.repository = repository } @@ -18,4 +19,8 @@ final class PostsUseCase: Domain.PostsUseCase { func save(post: Post) -> Observable { return repository.save(entity: post) } + + func delete(post: Post) -> Observable { + return repository.delete(entity: post) + } } diff --git a/RealmPlatform/Utility/Extensions/Realm+Ext.swift b/RealmPlatform/Utility/Extensions/Realm+Ext.swift index b71a929f..5e664c14 100644 --- a/RealmPlatform/Utility/Extensions/Realm+Ext.swift +++ b/RealmPlatform/Utility/Extensions/Realm+Ext.swift @@ -37,9 +37,12 @@ extension Reactive where Base: Realm { 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(entity.asRealm()) + self.base.delete(object) } + observer.onNext() observer.onCompleted() } catch {