From 4f3fa66a2397ca91947a157d0bb76d9129072e09 Mon Sep 17 00:00:00 2001 From: dev-Lena Date: Sun, 24 Jan 2021 23:29:57 +0900 Subject: [PATCH 1/3] feat: implement tag suggestion feature #43 --- PhotoTag/PhotoTag.xcodeproj/project.pbxproj | 4 + .../AppViewControllersFactory.swift | 4 +- .../Coordinator/PhotoNoteCoordinator.swift | 4 +- PhotoTag/PhotoTag/Network/Endpoint.swift | 2 + PhotoTag/PhotoTag/Network/UseCase.swift | 13 +++- .../Model/NoteNetworkingManager.swift | 30 ++++++++ .../Photo Note/Model/TagSuggestion.swift | 12 +++ .../PhotoNote/View/NoteViewController.swift | 76 ++++++++++++++++++- .../View/PhotoNoteViewController.swift | 3 +- .../ViewModel/PhotoNoteListViewModel.swift | 3 - .../Tag/Model/TagNetworkingManager.swift | 2 +- PhotoTag/PhotoTag/Utility/Typealias.swift | 1 + 12 files changed, 142 insertions(+), 12 deletions(-) create mode 100644 PhotoTag/PhotoTag/Photo Note/Model/TagSuggestion.swift diff --git a/PhotoTag/PhotoTag.xcodeproj/project.pbxproj b/PhotoTag/PhotoTag.xcodeproj/project.pbxproj index c9469d5..1488534 100644 --- a/PhotoTag/PhotoTag.xcodeproj/project.pbxproj +++ b/PhotoTag/PhotoTag.xcodeproj/project.pbxproj @@ -137,6 +137,7 @@ 6AD3ED6725519FE900F8EF61 /* UseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD3ED6625519FE900F8EF61 /* UseCase.swift */; }; 6AD3ED6C2551A09900F8EF61 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD3ED6B2551A09900F8EF61 /* HTTPMethod.swift */; }; 6AD3ED712551A0B400F8EF61 /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD3ED702551A0B400F8EF61 /* NetworkError.swift */; }; + 6AD8DAB225BDB63D003205C0 /* TagSuggestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8DAB125BDB63D003205C0 /* TagSuggestion.swift */; }; 6AE1245E256830A300291388 /* UISwipeGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AE1245D256830A300291388 /* UISwipeGestureRecognizer.swift */; }; 6AEB5545259CA8220044699D /* AlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AEB5544259CA8220044699D /* AlertView.swift */; }; 6AEB554A259CB2640044699D /* PhotoNoteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AEB5549259CB2640044699D /* PhotoNoteViewModel.swift */; }; @@ -252,6 +253,7 @@ 6AD3ED6625519FE900F8EF61 /* UseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UseCase.swift; sourceTree = ""; }; 6AD3ED6B2551A09900F8EF61 /* HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; 6AD3ED702551A0B400F8EF61 /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; + 6AD8DAB125BDB63D003205C0 /* TagSuggestion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TagSuggestion.swift; sourceTree = ""; }; 6AE1245D256830A300291388 /* UISwipeGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UISwipeGestureRecognizer.swift; sourceTree = ""; }; 6AEB5544259CA8220044699D /* AlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertView.swift; sourceTree = ""; }; 6AEB5549259CB2640044699D /* PhotoNoteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoNoteViewModel.swift; sourceTree = ""; }; @@ -314,6 +316,7 @@ children = ( 6A0A216725AAC158009D56DE /* NoteNetworkingManager.swift */, 6AC3ED1D25B821EA0032FF29 /* PhotoNote.swift */, + 6AD8DAB125BDB63D003205C0 /* TagSuggestion.swift */, ); path = Model; sourceTree = ""; @@ -918,6 +921,7 @@ 6AAE725E2555988B00CF7F9F /* TagCategoryView.swift in Sources */, 6A57CAEF255ED33600E42348 /* UsesAutoLayout.swift in Sources */, 6A63BA2C255C2F210096A0F7 /* TagManagementTableViewDelegate.swift in Sources */, + 6AD8DAB225BDB63D003205C0 /* TagSuggestion.swift in Sources */, 6A5544C925599CDF003F3864 /* UIColor.swift in Sources */, 6A5544E42559B3CA003F3864 /* ScrollableContentViewWithHeader.swift in Sources */, 6AEC28AE259223CC00D0634A /* URLComponents.swift in Sources */, diff --git a/PhotoTag/PhotoTag/AppConfigurator/AppViewControllersFactory.swift b/PhotoTag/PhotoTag/AppConfigurator/AppViewControllersFactory.swift index 5396af1..6f3ba89 100644 --- a/PhotoTag/PhotoTag/AppConfigurator/AppViewControllersFactory.swift +++ b/PhotoTag/PhotoTag/AppConfigurator/AppViewControllersFactory.swift @@ -31,8 +31,8 @@ struct AppViewControllersFactory { return SelectPhotoViewController(coordinator: coordinator) } - func writePhotoNoteViewController(coordinator: PhotoNoteCoordinator, with text: String) -> UIViewController { - let noteViewController = NoteViewController(coordinator: coordinator, with: text) + func writePhotoNoteViewController(coordinator: PhotoNoteCoordinator, with text: String, and photos: [NoteImage]) -> UIViewController { + let noteViewController = NoteViewController(coordinator: coordinator, with: text, and: photos) return noteViewController } diff --git a/PhotoTag/PhotoTag/Coordinator/PhotoNoteCoordinator.swift b/PhotoTag/PhotoTag/Coordinator/PhotoNoteCoordinator.swift index f92eb2d..170f6d4 100644 --- a/PhotoTag/PhotoTag/Coordinator/PhotoNoteCoordinator.swift +++ b/PhotoTag/PhotoTag/Coordinator/PhotoNoteCoordinator.swift @@ -24,8 +24,8 @@ final class PhotoNoteCoordinator: ChildCoordinator { navigationController.pushViewController(selectPhotoViewController, animated: true) } - func navigateToWritePhotoNote(with text: String) { - let writePhotoNoteViewController = appViewControllerFactory.writePhotoNoteViewController(coordinator: self, with: text) + func navigateToWritePhotoNote(with text: String, photos: [NoteImage]) { + let writePhotoNoteViewController = appViewControllerFactory.writePhotoNoteViewController(coordinator: self, with: text, and: photos) navigationController.pushViewController(writePhotoNoteViewController, animated: true) } diff --git a/PhotoTag/PhotoTag/Network/Endpoint.swift b/PhotoTag/PhotoTag/Network/Endpoint.swift index 236519a..0d32ef2 100644 --- a/PhotoTag/PhotoTag/Network/Endpoint.swift +++ b/PhotoTag/PhotoTag/Network/Endpoint.swift @@ -24,6 +24,7 @@ struct Endpoint: RequestProviding { case createNote case fetchPhotoNoteList case fetchPhotoNote + case tagSuggestion var description: String { switch self { @@ -34,6 +35,7 @@ struct Endpoint: RequestProviding { case .createNote: return "/notes" case .fetchPhotoNoteList: return "/tags" case .fetchPhotoNote: return "/notes/" + case .tagSuggestion: return "/suggestion" } } } diff --git a/PhotoTag/PhotoTag/Network/UseCase.swift b/PhotoTag/PhotoTag/Network/UseCase.swift index 2db3b88..5b18d83 100644 --- a/PhotoTag/PhotoTag/Network/UseCase.swift +++ b/PhotoTag/PhotoTag/Network/UseCase.swift @@ -65,7 +65,7 @@ struct UseCase { .eraseToAnyPublisher() } - // send file + // send file (send file and return HTTPURLResponse) func request(_ network: NetworkConnectable = NetworkManager.shared, request: URLRequest) -> AnyPublisher { return network @@ -75,6 +75,17 @@ struct UseCase { .eraseToAnyPublisher() } + // tag suggestion (send file and return two string arrays) + func request(_ network: NetworkConnectable = NetworkManager.shared, + urlRequest: URLRequest) -> AnyPublisher { + return network + .session + .dataTaskPublisher(for: urlRequest) + .map { $0.data } + .decode(type: TagSuggestion.self, decoder: decoder) + .eraseToAnyPublisher() + } + func request(_ network: NetworkConnectable = NetworkManager.shared, type: D.Type, endpoint: RequestProviding, method: HTTPMethod) -> AnyPublisher { diff --git a/PhotoTag/PhotoTag/Photo Note/Model/NoteNetworkingManager.swift b/PhotoTag/PhotoTag/Photo Note/Model/NoteNetworkingManager.swift index 34e1183..38f3f55 100644 --- a/PhotoTag/PhotoTag/Photo Note/Model/NoteNetworkingManager.swift +++ b/PhotoTag/PhotoTag/Photo Note/Model/NoteNetworkingManager.swift @@ -112,6 +112,36 @@ final class NoteNetworkingManager { })) } + // MARK: - fetch tag recommendation + func fetchTagRecommendation(images: [NoteImage], + completion: @escaping(TagSuggestion) -> Void) { + + let boundary = generateBoundaryString() + guard let endpoint = Endpoint(path: .tagSuggestion).url else { return } + var request = URLRequest(urlWithToken: endpoint, method: .get) + request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + + let httpBody = NSMutableData() + + // photo Image Data + for image in images { + guard let imageData = image.jpegData(compressionQuality: 0.1) else { return } + httpBody.append(convertFileData(fieldName: "photo", fileName: "\(Date().millisecondsSince1970)_photo.jpg", mimeType: "multipart/form-data", fileData: imageData, using: boundary)) + } + httpBody.appendString("--\(boundary)--") // add final boundary with the two trailing dashes + request.httpBody = httpBody as Data + + // request + UseCase.shared + .request(urlRequest: request) + .receive(subscriber: Subscribers.Sink(receiveCompletion: { [weak self] in + guard case let .failure(error) = $0 else { return } + debugPrint(error.localizedDescription) + }, receiveValue: { data in + completion(data) + })) + } + } extension NoteNetworkingManager { diff --git a/PhotoTag/PhotoTag/Photo Note/Model/TagSuggestion.swift b/PhotoTag/PhotoTag/Photo Note/Model/TagSuggestion.swift new file mode 100644 index 0000000..94015e4 --- /dev/null +++ b/PhotoTag/PhotoTag/Photo Note/Model/TagSuggestion.swift @@ -0,0 +1,12 @@ +// +// TagSuggestion.swift +// Pods +// +// Created by Keunna Lee on 2021/01/24. +// + +import Foundation + +struct TagSuggestion: Codable { + let tagsEn, tagsKr: [String] +} diff --git a/PhotoTag/PhotoTag/Photo Note/PhotoNote/View/NoteViewController.swift b/PhotoTag/PhotoTag/Photo Note/PhotoNote/View/NoteViewController.swift index 1a3f1e9..1e7ac4d 100644 --- a/PhotoTag/PhotoTag/Photo Note/PhotoNote/View/NoteViewController.swift +++ b/PhotoTag/PhotoTag/Photo Note/PhotoNote/View/NoteViewController.swift @@ -10,14 +10,17 @@ import UIKit final class NoteViewController: UIViewController { @IBOutlet weak var noteTextView: UITextView! + private let noteNetworkingManager = NoteNetworkingManager() weak var coordinator: PhotoNoteCoordinator? static let contentTextKey = "contentText" private var contentText: NoteText = "" private var existingText = "" + private var photos: [NoteImage] - init(coordinator: PhotoNoteCoordinator, with text: NoteText) { + init(coordinator: PhotoNoteCoordinator, with text: NoteText, and photos: [NoteImage]) { self.coordinator = coordinator self.existingText = text + self.photos = photos super.init(nibName: nil, bundle: nil) } @@ -26,11 +29,47 @@ final class NoteViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() setupView() + showTagRecommendation() } - + + @objc func tagTapped(_ sender: UIButton) { + guard let buttonTitle = sender.title(for: .normal) else { return } + DispatchQueue.main.async { + self.contentText += buttonTitle + self.noteTextView.text += buttonTitle + } + } + + private func showTagRecommendation() { + + fetchTagRecommendation { tagSuggestions in + for tag in tagSuggestions { + let tagButton = self.tagButton(title: "#\(tag)") + self.tagStackView.addArrangedSubview(tagButton) + } + let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50)) + scrollView.contentSize = CGSize(width: self.tagStackView.frame.width, height: self.tagStackView.frame.height) + scrollView.addSubview(self.tagStackView) + scrollView.sizeToFit() + self.noteTextView.inputAccessoryView = scrollView + } + + } + + private func fetchTagRecommendation( completionHandler: @escaping ([TagName]) -> Void) { + noteNetworkingManager.fetchTagRecommendation(images: photos) { tagSuggestion in + var tagSuggestions: [TagName] = [] + tagSuggestions.append(contentsOf: tagSuggestion.tagsEn) + tagSuggestions.append(contentsOf: tagSuggestion.tagsKr) + completionHandler(tagSuggestions) + } + } + private func setupView() { noteTextView.text = contentText noteTextView.text = existingText + noteTextView.becomeFirstResponder() + noteTextView.keyboardAppearance = .dark setupNoteTextView() } @@ -74,4 +113,37 @@ extension NoteViewController: UITextViewDelegate { let changedText = currentText.replacingCharacters(in: stringRange, with: text) return changedText.count <= maximumCount } + + func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { + return true + } +} + +extension NoteViewController { + private var tagStackView: UIStackView { + let stackView = UIStackView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50)) + stackView.axis = .horizontal + stackView.sizeToFit() + stackView.alignment = .fill + stackView.distribution = .equalSpacing + stackView.spacing = 5 + stackView.backgroundColor = .lightGray + stackView.contentMode = .scaleToFill + stackView.clipsToBounds = false + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + } + + private func tagButton(title: String) -> UIButton { + let button = UIButton() + button.backgroundColor = .lightGray + button.backgroundColor = .white + button.setTitleColor(.black, for: .normal) + button.setTitle("#\(title)", for: .normal) + button.frame = CGRect(x: 0, y: 0, width: button.intrinsicContentSize.width + 18, height: 50) + button.addTarget(self, action: #selector(self.tagTapped), for: .touchUpInside) + self.tagStackView.addArrangedSubview(button) + button.translatesAutoresizingMaskIntoConstraints = false + return button + } } diff --git a/PhotoTag/PhotoTag/Photo Note/PhotoNote/View/PhotoNoteViewController.swift b/PhotoTag/PhotoTag/Photo Note/PhotoNote/View/PhotoNoteViewController.swift index a23be92..79b1b25 100644 --- a/PhotoTag/PhotoTag/Photo Note/PhotoNote/View/PhotoNoteViewController.swift +++ b/PhotoTag/PhotoTag/Photo Note/PhotoNote/View/PhotoNoteViewController.swift @@ -28,6 +28,7 @@ class PhotoNoteViewController: UIViewController { @IBOutlet weak var imageHorizontalScrollView: UIScrollView! private var noteState: NoteState private var noteContentText: NoteText = "" + private var tagNames: [TagName] = [] private let noteNetworkManager = NoteNetworkingManager() init(coordinator: PhotoNoteCoordinator, @@ -158,7 +159,7 @@ class PhotoNoteViewController: UIViewController { } @objc private func presentNoteWritingScene() { - coordinator?.navigateToWritePhotoNote(with: noteContentText) + coordinator?.navigateToWritePhotoNote(with: noteContentText, photos: viewModel.selectedImages.value) } @objc func saveNoteText(_ notification: Notification) { diff --git a/PhotoTag/PhotoTag/Photo Note/PhotoNoteList/ViewModel/PhotoNoteListViewModel.swift b/PhotoTag/PhotoTag/Photo Note/PhotoNoteList/ViewModel/PhotoNoteListViewModel.swift index 67112bf..7cf59a5 100644 --- a/PhotoTag/PhotoTag/Photo Note/PhotoNoteList/ViewModel/PhotoNoteListViewModel.swift +++ b/PhotoTag/PhotoTag/Photo Note/PhotoNoteList/ViewModel/PhotoNoteListViewModel.swift @@ -30,9 +30,6 @@ class PhotoNoteListViewModel { noteNetworkingManager.fetchNoteList(tagIds: selectedTags) { photoList in guard let allPhotoList = photoList else { return } self.photoNoteList.value = allPhotoList - self.firstSelectedTagText.value = self.photoNoteList.value[0].tags[0] - self.secondSelectedTagText.value = self.photoNoteList.value[1].tags[0] - self.thirdSelectedTagText.value = self.photoNoteList.value[2].tags[0] completionHandler(allPhotoList) } } diff --git a/PhotoTag/PhotoTag/Tag/Model/TagNetworkingManager.swift b/PhotoTag/PhotoTag/Tag/Model/TagNetworkingManager.swift index 2be800c..1619ef6 100644 --- a/PhotoTag/PhotoTag/Tag/Model/TagNetworkingManager.swift +++ b/PhotoTag/PhotoTag/Tag/Model/TagNetworkingManager.swift @@ -26,7 +26,7 @@ final class TagNetworkingManager { })) } - func updateHashtagActivatedState(of tagId: Int, with data: HastagState) { + func updateHashtagActivatedState(of tagId: TagID, with data: HastagState) { UseCase.shared.request(data: data, endpoint: Endpoint.hashtagPatch(path: .patchHashtags, tagId: tagId), method: .patch) diff --git a/PhotoTag/PhotoTag/Utility/Typealias.swift b/PhotoTag/PhotoTag/Utility/Typealias.swift index 4de8794..d6a138b 100644 --- a/PhotoTag/PhotoTag/Utility/Typealias.swift +++ b/PhotoTag/PhotoTag/Utility/Typealias.swift @@ -9,6 +9,7 @@ import Foundation import UIKit.UIImage // tag +typealias TagName = String typealias TagID = Int typealias TagImage = UIImage From efd815bd29a946918eff9eddb3ada3daedbdb6ac Mon Sep 17 00:00:00 2001 From: dev-Lena Date: Sun, 14 Mar 2021 23:12:19 +0900 Subject: [PATCH 2/3] feat: activate button in tag category --- .../Tag/TagCategory/View/TagCategoryViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/PhotoTag/PhotoTag/Tag/TagCategory/View/TagCategoryViewController.swift b/PhotoTag/PhotoTag/Tag/TagCategory/View/TagCategoryViewController.swift index 383ef71..9d75959 100644 --- a/PhotoTag/PhotoTag/Tag/TagCategory/View/TagCategoryViewController.swift +++ b/PhotoTag/PhotoTag/Tag/TagCategory/View/TagCategoryViewController.swift @@ -55,6 +55,7 @@ final class TagCategoryViewController: UIViewController { fetchTags() configure() viewAppeared = true + activateButton() } // MARK: - Functions From 581589405c165db56047463bfa433cf34396584e Mon Sep 17 00:00:00 2001 From: dev-Lena Date: Wed, 24 Mar 2021 22:06:28 +0900 Subject: [PATCH 3/3] chore: delete label default text in detail page xib --- .../PhotoNote/View/PhotoNoteViewController.xib | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/PhotoTag/PhotoTag/Photo Note/PhotoNote/View/PhotoNoteViewController.xib b/PhotoTag/PhotoTag/Photo Note/PhotoNote/View/PhotoNoteViewController.xib index 9e89217..21a9071 100644 --- a/PhotoTag/PhotoTag/Photo Note/PhotoNote/View/PhotoNoteViewController.xib +++ b/PhotoTag/PhotoTag/Photo Note/PhotoNote/View/PhotoNoteViewController.xib @@ -1,9 +1,9 @@ - + - + @@ -117,14 +117,14 @@ -