diff --git a/Secretly.xcodeproj/project.pbxproj b/Secretly.xcodeproj/project.pbxproj index 8dd4829..55229a0 100644 --- a/Secretly.xcodeproj/project.pbxproj +++ b/Secretly.xcodeproj/project.pbxproj @@ -56,6 +56,9 @@ 30C77CB6266AF48300A888DC /* CurrentUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C77CB5266AF48300A888DC /* CurrentUser.swift */; }; 30C77CB8266BD44300A888DC /* CreatePostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C77CB7266BD44300A888DC /* CreatePostViewController.swift */; }; 30FD0E722659645A006E309A /* Faker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FD0E712659645A006E309A /* Faker.swift */; }; + 657AE4DF26BE032B00953718 /* Author.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657AE4DE26BE032B00953718 /* Author.swift */; }; + 657AE4E126BE037300953718 /* Like.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657AE4E026BE037300953718 /* Like.swift */; }; + 657AE4E926BE229E00953718 /* LikesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657AE4E826BE229E00953718 /* LikesService.swift */; }; E021984723FA35E00025C28E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E021984623FA35E00025C28E /* AppDelegate.swift */; }; E021984923FA35E00025C28E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E021984823FA35E00025C28E /* SceneDelegate.swift */; }; E021984B23FA35E00025C28E /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E021984A23FA35E00025C28E /* WelcomeViewController.swift */; }; @@ -125,6 +128,9 @@ 30C77CB5266AF48300A888DC /* CurrentUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentUser.swift; sourceTree = ""; }; 30C77CB7266BD44300A888DC /* CreatePostViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePostViewController.swift; sourceTree = ""; }; 30FD0E712659645A006E309A /* Faker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Faker.swift; sourceTree = ""; }; + 657AE4DE26BE032B00953718 /* Author.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Author.swift; sourceTree = ""; }; + 657AE4E026BE037300953718 /* Like.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Like.swift; sourceTree = ""; }; + 657AE4E826BE229E00953718 /* LikesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LikesService.swift; sourceTree = ""; }; E021984323FA35E00025C28E /* Secretly.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretly.app; sourceTree = BUILT_PRODUCTS_DIR; }; E021984623FA35E00025C28E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; E021984823FA35E00025C28E /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -222,6 +228,7 @@ 30C77CAF266AD69700A888DC /* CurrentUserService.swift */, 304E06C726742BDA00A99128 /* CreatePostService.swift */, 304E06C926742CC500A99128 /* FeedService.swift */, + 657AE4E826BE229E00953718 /* LikesService.swift */, ); path = Services; sourceTree = ""; @@ -248,6 +255,8 @@ 307A30572661AD540020DF8B /* User.swift */, 30C77CB3266AF47300A888DC /* Credentials.swift */, 30C77CB5266AF48300A888DC /* CurrentUser.swift */, + 657AE4DE26BE032B00953718 /* Author.swift */, + 657AE4E026BE037300953718 /* Like.swift */, ); path = Models; sourceTree = ""; @@ -445,6 +454,7 @@ 302B5849267E658E007133E6 /* RequestError.swift in Sources */, 302B5846267E658E007133E6 /* AmacaConfig.swift in Sources */, 302BB61C267D7CC800FD74F5 /* PreviewPostVIew.swift in Sources */, + 657AE4E126BE037300953718 /* Like.swift in Sources */, 3033795D267537B40066D94A /* FeedCollectionViewController+UICollectionViewDelegateFlowLayout .swift in Sources */, 30BC8BA82662CEBA00F7E6A5 /* Checksum.swift in Sources */, 30FD0E722659645A006E309A /* Faker.swift in Sources */, @@ -453,11 +463,13 @@ 307A305E2661CD510020DF8B /* PostCollectionViewCell.swift in Sources */, 302BB626267E447900FD74F5 /* PostInputViewController+UITextFieldDelegate.swift in Sources */, 30BC8BA02662B8A700F7E6A5 /* StorageType.swift in Sources */, + 657AE4E926BE229E00953718 /* LikesService.swift in Sources */, 307A305B2661B7A20020DF8B /* FeedCollectionViewController.swift in Sources */, 304E06CC2674442800A99128 /* UIImage+encodeBase64.swift in Sources */, 30BC8BA62662C02300F7E6A5 /* CacheImage.swift in Sources */, 302B5847267E658E007133E6 /* StatusCode.swift in Sources */, 302B584B267E658E007133E6 /* RequestBuilder.swift in Sources */, + 657AE4DF26BE032B00953718 /* Author.swift in Sources */, 307A306526629B990020DF8B /* AuthorView.swift in Sources */, 30C77CB4266AF47300A888DC /* Credentials.swift in Sources */, 30BC8BA22662BB0000F7E6A5 /* DataContainer.swift in Sources */, diff --git a/Secretly/Models/Author.swift b/Secretly/Models/Author.swift new file mode 100644 index 0000000..7382f9b --- /dev/null +++ b/Secretly/Models/Author.swift @@ -0,0 +1,20 @@ +// +// Author.swift +// Secretly +// +// Created by Victor Aceves on 06/08/21. +// Copyright © 2021 3zcurdia. All rights reserved. +// + +import Foundation +import UIKit + +struct Author: Restable { + let name: String? + let id: String? + + init(id: String, name: String){ + self.name = name + self.id = id + } +} diff --git a/Secretly/Models/Like.swift b/Secretly/Models/Like.swift new file mode 100644 index 0000000..0a5980e --- /dev/null +++ b/Secretly/Models/Like.swift @@ -0,0 +1,19 @@ +// +// Like.swift +// Secretly +// +// Created by Victor Aceves on 06/08/21. +// Copyright © 2021 3zcurdia. All rights reserved. +// + +import Foundation +import UIKit + +struct Like: Restable { + let id: Int + let likeableType: String + let likeableId: Int + let createdAt: String + let updatedAt: String + let user: User +} diff --git a/Secretly/Models/Post.swift b/Secretly/Models/Post.swift index eba5ff0..5f62945 100644 --- a/Secretly/Models/Post.swift +++ b/Secretly/Models/Post.swift @@ -21,8 +21,10 @@ struct Post: Restable { let longitude: Double? let createdAt: Date? let updatedAt: Date? - - init(content: String, backgroundColor: String, latitude: Double? = nil, longitude: Double? = nil, image: UIImage? = nil) { + var likesCount: Int? + var liked: Bool? + + init(content: String, backgroundColor: String, latitude: Double? = nil, longitude: Double? = nil, image: UIImage? = nil, likesCount: Int? = nil, liked:Bool?=false) { self.content = content self.backgroundColor = backgroundColor self.id = nil @@ -34,8 +36,10 @@ struct Post: Restable { self.commentsCount = nil self.createdAt = nil self.updatedAt = nil + self.likesCount = likesCount + self.liked = liked } - + func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(content, forKey: .content) @@ -44,4 +48,10 @@ struct Post: Restable { try container.encode(latitude, forKey: .latitude) try container.encode(longitude, forKey: .longitude) } + + mutating func toggleLike(isLiked:Bool){ + likesCount = isLiked ? (likesCount ?? 0) + 1 : (likesCount ?? 0) - 1 + self.liked = isLiked + } + } diff --git a/Secretly/Network/HttpResponse.swift b/Secretly/Network/HttpResponse.swift index eb0543a..45ec5e7 100644 --- a/Secretly/Network/HttpResponse.swift +++ b/Secretly/Network/HttpResponse.swift @@ -10,16 +10,20 @@ import Foundation struct HttpResponse { let httpUrlResponse: HTTPURLResponse - + init(response: URLResponse?) { - self.httpUrlResponse = (response as? HTTPURLResponse) ?? HTTPURLResponse() + httpUrlResponse = (response as? HTTPURLResponse) ?? HTTPURLResponse() } - + var status: StatusCode { - return StatusCode(rawValue: self.httpUrlResponse.statusCode) + return StatusCode(rawValue: httpUrlResponse.statusCode) } - + func result(for data: Data?) -> Result { - return status.result().map { _ in data } + if let udata = data, !udata.isEmpty { + return status.result().map { _ in data } + } else { + return status.result().map { _ in nil } + } } } diff --git a/Secretly/Network/RestClient.swift b/Secretly/Network/RestClient.swift index 3048ccf..dd53aee 100644 --- a/Secretly/Network/RestClient.swift +++ b/Secretly/Network/RestClient.swift @@ -20,7 +20,7 @@ typealias Restable = Codable & Identifiable struct RestClient { let client: HttpClient let path: String - + public var decoder: JSONDecoder = { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase @@ -32,25 +32,32 @@ struct RestClient { encoder.keyEncodingStrategy = .convertToSnakeCase return encoder }() - + func list(complete: @escaping (Result<[T], Error>) -> Void) { client.get(path: path) { result in let newResult = result.flatMap { parseList(data: $0) } complete(newResult) } } - + func show(complete: @escaping (Result) -> Void) { show("", complete: complete) } - + func show(_ identifier: String, complete: @escaping (Result) -> Void) { client.get(path: "\(path)/\(identifier)") { result in let newResult = result.flatMap { parse(data: $0) } complete(newResult) } } - + + func create(complete: @escaping (Result) -> Void) throws { + client.post(path: path, body: nil) { result in + let newResult = result.flatMap { parse(data: $0) } + complete(newResult) + } + } + func create(model: T, complete: @escaping (Result) -> Void) throws { let data = try encoder.encode(model) client.post(path: path, body: data) { result in @@ -58,7 +65,7 @@ struct RestClient { complete(newResult) } } - + func update(model: T, complete: @escaping (Result) -> Void) throws { let data = try encoder.encode(model) client.put(path: "\(path)/\(model.id)", body: data) { result in @@ -66,14 +73,21 @@ struct RestClient { complete(newResult) } } - + + func delete(complete: @escaping (Result) -> Void) { + client.delete(path: path) { result in + let newResult = result.flatMap { parse(data: $0) } + complete(newResult) + } + } + func delete(model: T, complete: @escaping (Result) -> Void) { client.delete(path: "\(path)/\(model.id)") { result in let newResult = result.flatMap { parse(data: $0) } complete(newResult) } } - + private func parseList(data: Data?) -> Result<[T], Error> { if let data = data { return Result { try self.decoder.decode([T].self, from: data) } @@ -81,7 +95,7 @@ struct RestClient { return .success([]) } } - + private func parse(data: Data?) -> Result { if let data = data { return Result { try self.decoder.decode(T.self, from: data) } diff --git a/Secretly/Services/LikesService.swift b/Secretly/Services/LikesService.swift new file mode 100644 index 0000000..bf016db --- /dev/null +++ b/Secretly/Services/LikesService.swift @@ -0,0 +1,37 @@ +// +// LikesService.swift +// Secretly +// +// Created by Victor Aceves on 06/08/21. +// Copyright © 2021 3zcurdia. All rights reserved. +// + +import Foundation + +struct LikesService { + private var likesEndpoint: RestClient? + var liked = false + + init(post: Post?){ + guard let post = post, let postId = post.id else { + likesEndpoint = nil + return + } + likesEndpoint = RestClient(client: AmacaConfig.shared.httpClient, path: "/api/v1/posts/\(postId)/likes") + liked = post.liked ?? false + } + + mutating func toggleLike(complete: @escaping (Result) -> Void) { + if liked { + liked = !liked + likesEndpoint?.delete { result in + DispatchQueue.main.async { complete(result) } + } + } else { + liked = !liked + try? likesEndpoint?.create { result in + DispatchQueue.main.async { complete(result) } + } + } + } +} diff --git a/Secretly/Views/PostCollectionViewCell.swift b/Secretly/Views/PostCollectionViewCell.swift index ee08d53..abc6bdb 100644 --- a/Secretly/Views/PostCollectionViewCell.swift +++ b/Secretly/Views/PostCollectionViewCell.swift @@ -10,21 +10,24 @@ import UIKit class PostCollectionViewCell: UICollectionViewCell { static let reuseIdentifier = "feedPostCell" + var likesService: LikesService? var post: Post? { didSet { - updateView() + updateView() + likesService = LikesService(post: post) } } @IBOutlet weak var authorView: AuthorView! @IBOutlet weak var contentLabel: UILabel! @IBOutlet weak var imageView: UIImageView! - @IBOutlet weak var likeState: UIImageView! @IBOutlet weak var commentCounter: UILabel! - + @IBOutlet weak var likesCounter: UILabel! + @IBOutlet weak var btnLike: UIButton! + override func awakeFromNib() { super.awakeFromNib() } - + func updateView() { imageView.image = nil guard let post = post else { return } @@ -37,5 +40,30 @@ class PostCollectionViewCell: UICollectionViewCell { ImageLoader.load(postImg.mediumUrl) { img in self.imageView.image = img } } self.authorView.author = post.user + // Update likes counter label + self.likesCounter.text = "\(post.likesCount ?? 0) Me gusta" + // Update like image + if post.liked ?? false{ + btnLike.setImage(UIImage(systemName: "heart.fill"), for: .normal) + } else{ + btnLike.setImage(UIImage(systemName: "heart"), for: .normal) + } + } + + @IBAction func toggleLike(_ sender: UIButton) { + likesService?.toggleLike { [unowned self] result in + switch result { + case .success(nil): + btnLike.setImage(UIImage(systemName: "heart"), for: .normal) + post?.toggleLike(isLiked: false) + case .success: + btnLike.setImage(UIImage(systemName: "heart.fill"), for: .normal) + post?.toggleLike(isLiked: true) + case .failure: + print("Failure request \(result)") + } + self.likesCounter.text = "\(post?.likesCount ?? 0) Me gusta" + } } + } diff --git a/Secretly/Views/PostCollectionViewCell.xib b/Secretly/Views/PostCollectionViewCell.xib index 7b3a4e2..c93afa6 100644 --- a/Secretly/Views/PostCollectionViewCell.xib +++ b/Secretly/Views/PostCollectionViewCell.xib @@ -30,14 +30,6 @@ - - - - - - - -