diff --git a/Particle/Particle.xcodeproj/project.pbxproj b/Particle/Particle.xcodeproj/project.pbxproj index a7f1a2d..b4a3741 100644 --- a/Particle/Particle.xcodeproj/project.pbxproj +++ b/Particle/Particle.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ @@ -38,6 +38,7 @@ 3520EF442A74DDA700A53B34 /* YdestreetB.otf in Resources */ = {isa = PBXBuildFile; fileRef = 3520EF422A74DDA700A53B34 /* YdestreetB.otf */; }; 3520EF452A74DDA700A53B34 /* YdestreetL.otf in Resources */ = {isa = PBXBuildFile; fileRef = 3520EF432A74DDA700A53B34 /* YdestreetL.otf */; }; 3520EF472A74DFBD00A53B34 /* UIFont+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3520EF462A74DFBD00A53B34 /* UIFont+.swift */; }; + AF0D09DB2A823001003EE03D /* UIView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF0D09DA2A823001003EE03D /* UIView+.swift */; }; AF0D71C52A5BE6A200157758 /* AppComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF0D71C42A5BE6A200157758 /* AppComponent.swift */; }; AF0D71CB2A5BE9B600157758 /* RootRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF0D71C72A5BE9B600157758 /* RootRouter.swift */; }; AF0D71CC2A5BE9B600157758 /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF0D71C82A5BE9B600157758 /* RootViewController.swift */; }; @@ -65,6 +66,7 @@ AF1D32B02A680CC700980F14 /* PhotoPickerInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1D32AC2A680CC700980F14 /* PhotoPickerInteractor.swift */; }; AF4A19112A6A1E4B00932372 /* PhotoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4A19102A6A1E4B00932372 /* PhotoCell.swift */; }; AF4A19132A6A59BB00932372 /* UIImageView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4A19122A6A59BA00932372 /* UIImageView+.swift */; }; + AF6059F92A75FEA600FE1565 /* SelectedPhotoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF6059F82A75FEA600FE1565 /* SelectedPhotoCell.swift */; }; AFB4BDC52A4093DC008BA9E0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFB4BDC42A4093DC008BA9E0 /* AppDelegate.swift */; }; AFB4BDC72A4093DC008BA9E0 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFB4BDC62A4093DC008BA9E0 /* SceneDelegate.swift */; }; AFB4BDCE2A4093DD008BA9E0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AFB4BDCD2A4093DD008BA9E0 /* Assets.xcassets */; }; @@ -137,6 +139,7 @@ 3520EF422A74DDA700A53B34 /* YdestreetB.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = YdestreetB.otf; sourceTree = ""; }; 3520EF432A74DDA700A53B34 /* YdestreetL.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = YdestreetL.otf; sourceTree = ""; }; 3520EF462A74DFBD00A53B34 /* UIFont+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+.swift"; sourceTree = ""; }; + AF0D09DA2A823001003EE03D /* UIView+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+.swift"; sourceTree = ""; }; AF0D71C42A5BE6A200157758 /* AppComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppComponent.swift; sourceTree = ""; }; AF0D71C72A5BE9B600157758 /* RootRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouter.swift; sourceTree = ""; }; AF0D71C82A5BE9B600157758 /* RootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewController.swift; sourceTree = ""; }; @@ -164,6 +167,7 @@ AF1D32AC2A680CC700980F14 /* PhotoPickerInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerInteractor.swift; sourceTree = ""; }; AF4A19102A6A1E4B00932372 /* PhotoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCell.swift; sourceTree = ""; }; AF4A19122A6A59BA00932372 /* UIImageView+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+.swift"; sourceTree = ""; }; + AF6059F82A75FEA600FE1565 /* SelectedPhotoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedPhotoCell.swift; sourceTree = ""; }; AFB4BDC12A4093DC008BA9E0 /* Particle.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Particle.app; sourceTree = BUILT_PRODUCTS_DIR; }; AFB4BDC42A4093DC008BA9E0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; AFB4BDC62A4093DC008BA9E0 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -278,13 +282,6 @@ path = Utils; sourceTree = ""; }; - 3520EF242A5FB7E400A53B34 /* AddArticleNavigation */ = { - isa = PBXGroup; - children = ( - ); - path = AddArticleNavigation; - sourceTree = ""; - }; 3520EF272A74DADE00A53B34 /* Fonts */ = { isa = PBXGroup; children = ( @@ -423,6 +420,14 @@ path = View; sourceTree = ""; }; + AF6059F72A75FE9100FE1565 /* View */ = { + isa = PBXGroup; + children = ( + AF6059F82A75FEA600FE1565 /* SelectedPhotoCell.swift */, + ); + path = View; + sourceTree = ""; + }; AF86D82C2A4E6030009F1E9D /* Entry */ = { isa = PBXGroup; children = ( @@ -526,6 +531,7 @@ AFBB5F6E2A66B60D002416DA /* SelectSentence */ = { isa = PBXGroup; children = ( + AF6059F72A75FE9100FE1565 /* View */, AFBB5F702A66B65D002416DA /* SelectSentenceRouter.swift */, AFBB5F712A66B65D002416DA /* SelectSentenceViewController.swift */, AFBB5F722A66B65D002416DA /* SelectSentenceBuilder.swift */, @@ -555,6 +561,7 @@ 3520EF462A74DFBD00A53B34 /* UIFont+.swift */, AFBB5F6C2A66B5C1002416DA /* UIViewController+.swift */, AF4A19122A6A59BA00932372 /* UIImageView+.swift */, + AF0D09DA2A823001003EE03D /* UIView+.swift */, ); path = Extension; sourceTree = ""; @@ -597,7 +604,7 @@ }; }; buildConfigurationList = AFB4BDBC2A4093DC008BA9E0 /* Build configuration list for PBXProject "Particle" */; - compatibilityVersion = "Xcode 13.0"; + compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -714,6 +721,7 @@ AFBB5F772A66B65D002416DA /* SelectSentenceInteractor.swift in Sources */, 3520EF192A5E91A900A53B34 /* SetAdditionalInformationInteractor.swift in Sources */, AF1945172A5C407500B9FFA1 /* Constant.swift in Sources */, + AF6059F92A75FEA600FE1565 /* SelectedPhotoCell.swift in Sources */, AF4A19112A6A1E4B00932372 /* PhotoCell.swift in Sources */, AF0D71DA2A5BEEAD00157758 /* RootComponent+LoggedOut.swift in Sources */, AFBB5F752A66B65D002416DA /* SelectSentenceViewController.swift in Sources */, @@ -743,6 +751,7 @@ 3520EF082A5E8F3400A53B34 /* UIImage+ParticleImage.swift in Sources */, AFBD9BC52A715FBC0076A404 /* UIColor+ParticleColor.swift in Sources */, AFB4BDC52A4093DC008BA9E0 /* AppDelegate.swift in Sources */, + AF0D09DB2A823001003EE03D /* UIView+.swift in Sources */, 3520EF042A5E6F9C00A53B34 /* OrganizingSentenceRepository.swift in Sources */, AF0D71CE2A5BE9B600157758 /* RootInteractor.swift in Sources */, 3520EEF52A5CD3C000A53B34 /* OrganizingSentenceInteractor.swift in Sources */, @@ -831,7 +840,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -885,7 +894,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -911,7 +920,7 @@ INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -941,7 +950,7 @@ INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Particle/Particle/Entry/AppDelegate.swift b/Particle/Particle/Entry/AppDelegate.swift index 4580a79..c4d4e96 100644 --- a/Particle/Particle/Entry/AppDelegate.swift +++ b/Particle/Particle/Entry/AppDelegate.swift @@ -38,6 +38,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return } } + + UserDefaults.standard.set(false, forKey: "ShowSwipeGuide") return true } diff --git a/Particle/Particle/Extension/UIImageView+.swift b/Particle/Particle/Extension/UIImageView+.swift index 0264d02..50867e9 100644 --- a/Particle/Particle/Extension/UIImageView+.swift +++ b/Particle/Particle/Extension/UIImageView+.swift @@ -10,7 +10,7 @@ import Photos extension UIImageView { - func fetchImage(asset: PHAsset, contentMode: PHImageContentMode, targetSize: CGSize) { + func fetchImage(asset: PHAsset, contentMode: PHImageContentMode, targetSize: CGSize, _ completion: ((CGFloat) -> Void)? = nil) { let options = PHImageRequestOptions() options.version = .original options.deliveryMode = .opportunistic @@ -21,11 +21,12 @@ extension UIImageView { targetSize: targetSize, contentMode: contentMode, options: options) { image, info in - if image == nil { - Console.error(#function) - print(info ?? #function) - } else { + + if let image = image { self.image = image + completion?(image.size.height / image.size.width) + } else { + Console.error("\(#function) image 가 존재하지 않습니다.") } } } diff --git a/Particle/Particle/Extension/UIView+.swift b/Particle/Particle/Extension/UIView+.swift new file mode 100644 index 0000000..78c85c5 --- /dev/null +++ b/Particle/Particle/Extension/UIView+.swift @@ -0,0 +1,23 @@ +// +// UIView+.swift +// Particle +// +// Created by 이원빈 on 2023/08/08. +// + +import UIKit + +extension UIView { + + func addRoundedCorner(corners: UIRectCorner, radius: CGFloat) { + let maskPath = UIBezierPath( + roundedRect: bounds, + byRoundingCorners: corners, + cornerRadii: CGSize(width: radius, height: radius) + ) + + let maskLayer = CAShapeLayer() + maskLayer.path = maskPath.cgPath + layer.mask = maskLayer + } +} diff --git a/Particle/Particle/LoggedIn/LoggedInBuilder.swift b/Particle/Particle/LoggedIn/LoggedInBuilder.swift index 6e02543..7750af1 100644 --- a/Particle/Particle/LoggedIn/LoggedInBuilder.swift +++ b/Particle/Particle/LoggedIn/LoggedInBuilder.swift @@ -8,19 +8,13 @@ import RIBs protocol LoggedInDependency: Dependency { - // TODO: Make sure to convert the variable into lower-camelcase. - var LoggedInViewController: LoggedInViewControllable { get } - var organizingSentenceRepository: OrganizingSentenceRepository { get } + var loggedInViewController: LoggedInViewControllable { get } } final class LoggedInComponent: Component, MainDependency { - // TODO: Make sure to convert the variable into lower-camelcase. - fileprivate var LoggedInViewController: LoggedInViewControllable { - return dependency.LoggedInViewController - } - var organizingSentenceRepository: OrganizingSentenceRepository { - return dependency.organizingSentenceRepository + fileprivate var loggedInViewController: LoggedInViewControllable { + return dependency.loggedInViewController } } @@ -43,7 +37,7 @@ final class LoggedInBuilder: Builder, LoggedInBuildable { let mainBuilder = MainBuilder(dependency: component) return LoggedInRouter( interactor: interactor, - viewController: component.LoggedInViewController, + viewController: component.loggedInViewController, mainBuilder: mainBuilder ) } diff --git a/Particle/Particle/Main/HomeTab/AddArticle/AddAriticleRepository/OrganizingSentenceRepository.swift b/Particle/Particle/Main/HomeTab/AddArticle/AddAriticleRepository/OrganizingSentenceRepository.swift index 953161b..ecca19e 100644 --- a/Particle/Particle/Main/HomeTab/AddArticle/AddAriticleRepository/OrganizingSentenceRepository.swift +++ b/Particle/Particle/Main/HomeTab/AddArticle/AddAriticleRepository/OrganizingSentenceRepository.swift @@ -14,9 +14,8 @@ public protocol OrganizingSentenceRepository { } public final class OrganizingSentenceRepositoryImp: OrganizingSentenceRepository { - public var sentenceFile: RxSwift.BehaviorSubject<[String]> { - sentenceSubject - } + + public var sentenceFile: BehaviorSubject<[String]> = .init(value: []) private let sentenceSubject = BehaviorSubject<[String]>(value: [ "그렇게 쓴 글은 매일 사회관계망서비스(SNS)에 남기기도 하고, 모아서 책으로 내기도 한다.", diff --git a/Particle/Particle/Main/HomeTab/AddArticle/AddArticleBuilder.swift b/Particle/Particle/Main/HomeTab/AddArticle/AddArticleBuilder.swift index 88a28b8..6f6bffd 100644 --- a/Particle/Particle/Main/HomeTab/AddArticle/AddArticleBuilder.swift +++ b/Particle/Particle/Main/HomeTab/AddArticle/AddArticleBuilder.swift @@ -8,20 +8,15 @@ import RIBs protocol AddArticleDependency: Dependency { - // TODO: Make sure to convert the variable into lower-camelcase. var addArticleViewController: ViewControllable { get } - // TODO: Declare the set of dependencies required by this RIB, but won't be - // created by this RIB. } final class AddArticleComponent: Component { - - // TODO: Make sure to convert the variable into lower-camelcase. + var repository = OrganizingSentenceRepositoryImp() + fileprivate var addArticleViewController: ViewControllable { return dependency.addArticleViewController } - - // TODO: Declare 'fileprivate' dependencies that are only used by this RIB. } // MARK: - Builder @@ -31,6 +26,7 @@ protocol AddArticleBuildable: Buildable { } final class AddArticleBuilder: Builder, AddArticleBuildable { + override init(dependency: AddArticleDependency) { super.init(dependency: dependency) @@ -38,6 +34,7 @@ final class AddArticleBuilder: Builder, AddArticleBuildabl func build(withListener listener: AddArticleListener) -> AddArticleRouting { let component = AddArticleComponent(dependency: dependency) + let interactor = AddArticleInteractor() interactor.listener = listener @@ -63,6 +60,6 @@ extension AddArticleComponent: PhotoPickerDependency, SetAdditionalInformationDependency { var organizingSentenceRepository: OrganizingSentenceRepository { - return OrganizingSentenceRepositoryImp() //FIXME: ?? + return repository } } diff --git a/Particle/Particle/Main/HomeTab/AddArticle/AddArticleInteractor.swift b/Particle/Particle/Main/HomeTab/AddArticle/AddArticleInteractor.swift index f5d85f2..67b73c0 100644 --- a/Particle/Particle/Main/HomeTab/AddArticle/AddArticleInteractor.swift +++ b/Particle/Particle/Main/HomeTab/AddArticle/AddArticleInteractor.swift @@ -25,19 +25,14 @@ protocol AddArticleRouting: Routing { func detachSetAdditionalInformation() } -protocol AddArticleListener: AnyObject { - // TODO: Declare methods the interactor can invoke to communicate with other RIBs. -} +protocol AddArticleListener: AnyObject { } final class AddArticleInteractor: Interactor, AddArticleInteractable { weak var router: AddArticleRouting? weak var listener: AddArticleListener? - // TODO: Add additional dependencies to constructor. Do not perform any logic - // in constructor. - override init() { - } + override init() { } override func didBecomeActive() { super.didBecomeActive() @@ -48,7 +43,6 @@ final class AddArticleInteractor: Interactor, AddArticleInteractable { super.willResignActive() router?.cleanupViews() - // TODO: Pause any business logic. } // MARK: - PhotoPickerListener @@ -71,7 +65,6 @@ final class AddArticleInteractor: Interactor, AddArticleInteractable { router?.attachOrganizingSentence() } - // MARK: - OrganizingSentenceListener func organizingSentenceNextButtonTapped() { diff --git a/Particle/Particle/Main/HomeTab/AddArticle/PhotoPicker/PhotoPickerViewController.swift b/Particle/Particle/Main/HomeTab/AddArticle/PhotoPicker/PhotoPickerViewController.swift index 77de943..92be6ed 100644 --- a/Particle/Particle/Main/HomeTab/AddArticle/PhotoPicker/PhotoPickerViewController.swift +++ b/Particle/Particle/Main/HomeTab/AddArticle/PhotoPicker/PhotoPickerViewController.swift @@ -16,7 +16,9 @@ protocol PhotoPickerPresentableListener: AnyObject { func nextButtonTapped(with images: [PHAsset]) } -final class PhotoPickerViewController: UIViewController, PhotoPickerPresentable, PhotoPickerViewControllable { +final class PhotoPickerViewController: UIViewController, + PhotoPickerPresentable, + PhotoPickerViewControllable { enum Metric { enum NavigationBar { @@ -31,7 +33,7 @@ final class PhotoPickerViewController: UIViewController, PhotoPickerPresentable, weak var listener: PhotoPickerPresentableListener? private var disposeBag: DisposeBag = .init() - private var selectedItems: [PHAsset] = [] + private var selectedIndexPaths: [IndexPath] = [] private let navigationBar: UIView = { let view = UIView() @@ -74,6 +76,7 @@ final class PhotoPickerViewController: UIViewController, PhotoPickerPresentable, return collectionView }() + // MARK: - Initializers init() { super.init(nibName: nil, bundle: nil) modalPresentationStyle = .fullScreen @@ -83,6 +86,7 @@ final class PhotoPickerViewController: UIViewController, PhotoPickerPresentable, fatalError("init(coder:) has not been implemented") } + // MARK: - View LifeCycles override func viewDidLoad() { super.viewDidLoad() setupInitialView() @@ -120,12 +124,18 @@ final class PhotoPickerViewController: UIViewController, PhotoPickerPresentable, nextButton.rx.tap .bind { [weak self] in - self?.listener?.nextButtonTapped(with: self?.selectedItems ?? []) + guard let self = self else { return } + self.listener?.nextButtonTapped(with: self.getSelectedPhotoes()) } .disposed(by: disposeBag) } private func bind() { + bindCollectionViewCell() + bindItemSelected() + } + + private func bindCollectionViewCell() { Observable.of(Array(0...(photoCount - 1))) .bind(to: photoCollectionView.rx.items( cellIdentifier: PhotoCell.defaultReuseIdentifier, @@ -133,36 +143,62 @@ final class PhotoPickerViewController: UIViewController, PhotoPickerPresentable, ) { index, item, cell in guard let allPhotos = allPhotos else { - Console.error("allPhotos 값이 존재하지 않습니다.") + Console.error("\(#function) allPhotos 값이 존재하지 않습니다.") return } cell.setImage(with: allPhotos.object(at: item)) } .disposed(by: disposeBag) - + } + + private func bindItemSelected() { photoCollectionView .rx .itemSelected .subscribe { [weak self] indexPath in - guard let self = self else { return } + guard let self = self, + let indexPath = indexPath.element else { return } - guard let allPhotos = allPhotos else { - Console.error("allPhotos 값이 존재하지 않습니다.") - return - } - let selectedPhoto = allPhotos.object(at: indexPath.element?.row ?? 0) - if self.selectedItems.contains(selectedPhoto) { - self.selectedItems.remove(at: self.selectedItems.firstIndex(of: selectedPhoto) ?? .zero) + if self.selectedIndexPaths.contains(indexPath) { + self.selectedIndexPaths.remove( + at: self.selectedIndexPaths.firstIndex(of: indexPath) ?? .zero + ) + guard let cell = self.getCell(at: indexPath) else { return } + cell.uncheck() } else { - self.selectedItems.append(selectedPhoto) + self.selectedIndexPaths.append(indexPath) } - guard let cell = self.photoCollectionView.cellForItem(at: indexPath.element ?? .init(item: 0, section: 0)) as? PhotoCell else { - return + + for (order, indexPath) in self.selectedIndexPaths.enumerated() { + guard let cell = self.getCell(at: indexPath) else { return } + cell.check(number: order + 1) } - cell.check(number: self.selectedItems.count) } .disposed(by: disposeBag) } + + private func getSelectedPhotoes() -> [PHAsset] { + guard let allPhotos = allPhotos else { + Console.error("\(#function) allPhotos 값이 존재하지 않습니다.") + return [] + } + let selectedPhotoes = selectedIndexPaths.map { + allPhotos.object(at: $0.row) + } + + return selectedPhotoes + } + + private func getCell(at indexPath: IndexPath?) -> PhotoCell? { + guard let indexPath = indexPath else { + Console.error("\(#function) indexPath가 존재하지 않습니다.") + return nil + } + guard let cell = self.photoCollectionView.cellForItem(at: indexPath) as? PhotoCell else { + return nil + } + return cell + } } // MARK: - Layout Settting diff --git a/Particle/Particle/Main/HomeTab/AddArticle/PhotoPicker/View/PhotoCell.swift b/Particle/Particle/Main/HomeTab/AddArticle/PhotoPicker/View/PhotoCell.swift index e99f8ae..dbf7a3a 100644 --- a/Particle/Particle/Main/HomeTab/AddArticle/PhotoPicker/View/PhotoCell.swift +++ b/Particle/Particle/Main/HomeTab/AddArticle/PhotoPicker/View/PhotoCell.swift @@ -70,16 +70,16 @@ final class PhotoCell: UICollectionViewCell { } func check(number: Int) { - if checkBox_checked.alpha == 0 { - dimmingView.alpha = 0.3 - checkBox_checked.alpha = 1 - numberLabel.alpha = 1 - numberLabel.text = "\(number)" - } else { - dimmingView.alpha = 0 - checkBox_checked.alpha = 0 - numberLabel.alpha = 0 - } + dimmingView.alpha = 0.3 + checkBox_checked.alpha = 1 + numberLabel.alpha = 1 + numberLabel.text = "\(number)" + } + + func uncheck() { + dimmingView.alpha = 0 + checkBox_checked.alpha = 0 + numberLabel.alpha = 0 } } diff --git a/Particle/Particle/Main/HomeTab/AddArticle/SelectSentence/EditSentence/EditSentenceBuilder.swift b/Particle/Particle/Main/HomeTab/AddArticle/SelectSentence/EditSentence/EditSentenceBuilder.swift index 06eb845..ad0caed 100644 --- a/Particle/Particle/Main/HomeTab/AddArticle/SelectSentence/EditSentence/EditSentenceBuilder.swift +++ b/Particle/Particle/Main/HomeTab/AddArticle/SelectSentence/EditSentence/EditSentenceBuilder.swift @@ -7,15 +7,9 @@ import RIBs -protocol EditSentenceDependency: Dependency { - // TODO: Declare the set of dependencies required by this RIB, but cannot be - // created by this RIB. -} - -final class EditSentenceComponent: Component { +protocol EditSentenceDependency: Dependency { } - // TODO: Declare 'fileprivate' dependencies that are only used by this RIB. -} +final class EditSentenceComponent: Component { } // MARK: - Builder @@ -31,7 +25,7 @@ final class EditSentenceBuilder: Builder, EditSentenceBu func build(withListener listener: EditSentenceListener, text: String) -> EditSentenceRouting { _ = EditSentenceComponent(dependency: dependency) - let viewController = EditSentenceViewController(with: text) // FIXME: text 데이터 전달과정 리뷰요청. component 활용방법 연구 + let viewController = EditSentenceViewController(with: text) let interactor = EditSentenceInteractor(presenter: viewController) interactor.listener = listener return EditSentenceRouter(interactor: interactor, viewController: viewController) diff --git a/Particle/Particle/Main/HomeTab/AddArticle/SelectSentence/EditSentence/EditSentenceInteractor.swift b/Particle/Particle/Main/HomeTab/AddArticle/SelectSentence/EditSentence/EditSentenceInteractor.swift index 8fb9fee..a43fe6a 100644 --- a/Particle/Particle/Main/HomeTab/AddArticle/SelectSentence/EditSentence/EditSentenceInteractor.swift +++ b/Particle/Particle/Main/HomeTab/AddArticle/SelectSentence/EditSentence/EditSentenceInteractor.swift @@ -18,8 +18,8 @@ protocol EditSentencePresentable: Presentable { } protocol EditSentenceListener: AnyObject { - // TODO: Declare methods the interactor can invoke to communicate with other RIBs. - func dismissEditSentence() + func dismissEditSentence(with text: String) + func swipeToNextPhoto() } final class EditSentenceInteractor: PresentableInteractor, @@ -48,7 +48,8 @@ final class EditSentenceInteractor: PresentableInteractor { - - // TODO: Declare 'fileprivate' dependencies that are only used by this RIB. + fileprivate var organizingSentenceRepository: OrganizingSentenceRepository { + return dependency.organizingSentenceRepository + } } // MARK: - Builder @@ -33,7 +33,7 @@ final class SelectSentenceBuilder: Builder, SelectSent func build(withListener listener: SelectSentenceListener, images: [PHAsset]) -> SelectSentenceRouting { let component = SelectSentenceComponent(dependency: dependency) let viewController = SelectSentenceViewController(selectedImages: images) - let interactor = SelectSentenceInteractor(presenter: viewController) + let interactor = SelectSentenceInteractor(presenter: viewController, repository: component.organizingSentenceRepository) interactor.listener = listener let editSentenceBuilder = EditSentenceBuilder(dependency: component) @@ -46,8 +46,4 @@ final class SelectSentenceBuilder: Builder, SelectSent } } -extension SelectSentenceComponent: EditSentenceDependency, OrganizingSentenceDependency { - var organizingSentenceRepository: OrganizingSentenceRepository { - OrganizingSentenceRepositoryImp() - } -} +extension SelectSentenceComponent: EditSentenceDependency { } diff --git a/Particle/Particle/Main/HomeTab/AddArticle/SelectSentence/SelectSentenceInteractor.swift b/Particle/Particle/Main/HomeTab/AddArticle/SelectSentence/SelectSentenceInteractor.swift index 90adc82..29bf9a4 100644 --- a/Particle/Particle/Main/HomeTab/AddArticle/SelectSentence/SelectSentenceInteractor.swift +++ b/Particle/Particle/Main/HomeTab/AddArticle/SelectSentence/SelectSentenceInteractor.swift @@ -17,6 +17,7 @@ protocol SelectSentenceRouting: ViewableRouting { protocol SelectSentencePresentable: Presentable { var listener: SelectSentencePresentableListener? { get set } // TODO: Declare methods the interactor can invoke the presenter to present data. + func showSwipeAnimation() } protocol SelectSentenceListener: AnyObject { @@ -28,13 +29,16 @@ protocol SelectSentenceListener: AnyObject { final class SelectSentenceInteractor: PresentableInteractor, SelectSentenceInteractable, SelectSentencePresentableListener { - + weak var router: SelectSentenceRouting? weak var listener: SelectSentenceListener? + private var organizingSentenceRepository: OrganizingSentenceRepository + // TODO: Add additional dependencies to constructor. Do not perform any logic // in constructor. - override init(presenter: SelectSentencePresentable) { + init(presenter: SelectSentencePresentable, repository: OrganizingSentenceRepository) { + self.organizingSentenceRepository = repository super.init(presenter: presenter) presenter.listener = self } @@ -56,12 +60,29 @@ final class SelectSentenceInteractor: PresentableInteractor 0 { - addCustomMenuItem() - } else { - UIMenuController.shared.menuItems = nil + let contentOffset = selectedPhotoCollectionView.contentOffset.x + let contentOffsetOnLastPage = DeviceSize.width * CGFloat(selectedImages.count-1) + guard contentOffset != contentOffsetOnLastPage else { + nextButton.isEnabled = true + return + } + UIView.animate(withDuration: 0.5, delay: 0) { + self.selectedPhotoCollectionView.contentOffset.x += DeviceSize.width } } } +extension SelectSentenceViewController: SelectedPhotoCellListener { + + func copyButtonTapped(with text: String) { + listener?.showEditSentenceModal(with: text) + } +} + // MARK: - Layout Settting private extension SelectSentenceViewController { func addSubviews() { - [backButton, navigationTitle].forEach { + [backButton, navigationTitle, nextButton].forEach { navigationBar.addSubview($0) } - [navigationBar, infoBox, textView].forEach { + [navigationBar, infoBox, selectedPhotoCollectionView].forEach { view.addSubview($0) } @@ -237,6 +280,11 @@ private extension SelectSentenceViewController { $0.left.equalToSuperview().inset(Metric.NavigationBar.backButtonLeftMargin) } + nextButton.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.right.equalToSuperview().inset(Metric.NavigationBar.nextButtonRightMargin) + } + navigationTitle.snp.makeConstraints { $0.center.equalToSuperview() } @@ -244,7 +292,7 @@ private extension SelectSentenceViewController { infoBox.snp.makeConstraints { $0.top.equalTo(navigationBar.snp.bottom) $0.leading.trailing.equalTo(view.safeAreaLayoutGuide) - $0.height.equalTo(53) + $0.height.equalTo(Metric.InfoBox.height) } infoLabel.snp.makeConstraints { @@ -252,7 +300,7 @@ private extension SelectSentenceViewController { $0.leading.equalToSuperview().offset(20) } - textView.snp.makeConstraints { + selectedPhotoCollectionView.snp.makeConstraints { $0.top.equalTo(infoBox.snp.bottom) $0.leading.trailing.bottom.equalTo(view.safeAreaLayoutGuide) } @@ -265,20 +313,9 @@ import SwiftUI @available(iOS 13.0, *) struct SelectSentenceViewController_Preview: PreviewProvider { + static var previews: some View { SelectSentenceViewController(selectedImages: []).showPreview() } } #endif - - -final class CustomTextView: UITextView { - override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { - return false - } - - override func buildMenu(with builder: UIMenuBuilder) { - builder.remove(menu: .lookup) - super.buildMenu(with: builder) - } -} diff --git a/Particle/Particle/Main/HomeTab/AddArticle/SelectSentence/View/SelectedPhotoCell.swift b/Particle/Particle/Main/HomeTab/AddArticle/SelectSentence/View/SelectedPhotoCell.swift new file mode 100644 index 0000000..d8440c4 --- /dev/null +++ b/Particle/Particle/Main/HomeTab/AddArticle/SelectSentence/View/SelectedPhotoCell.swift @@ -0,0 +1,136 @@ +// +// SelectedPhotoCell.swift +// Particle +// +// Created by 이원빈 on 2023/07/30. +// + +import UIKit +import Photos +import VisionKit + +protocol SelectedPhotoCellListener: AnyObject { + func copyButtonTapped(with text: String) +} + +final class SelectedPhotoCell: UICollectionViewCell { + + private let mainScrollView: UIScrollView = { + let scrollView = UIScrollView() + return scrollView + }() + + private let imageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + return imageView + }() + + private lazy var interaction: ImageAnalysisInteraction = { + let interaction = ImageAnalysisInteraction() + interaction.preferredInteractionTypes = .automatic + interaction.allowLongPressForDataDetectorsInTextMode = true + return interaction + }() + + private let imageAnalyzer = ImageAnalyzer() + + private var copiedText = "" + weak var listener: SelectedPhotoCellListener? + + override init(frame: CGRect) { + super.init(frame: frame) + addSubviews() + setConstraints() + contentView.clipsToBounds = true + NotificationCenter.default.addObserver( + self, + selector: #selector(copyButtonTapped), + name: UIPasteboard.changedNotification, + object: nil + ) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + override func prepareForReuse() { + super.prepareForReuse() + imageView.image = nil + } + + private func showLiveText() { + guard let image = imageView.image else { + Console.error("imageView.image == nil 입니다.") + return + } + + Task { + let configuration = ImageAnalyzer.Configuration([.text]) + + do { + let analysis = try await imageAnalyzer.analyze(image, configuration: configuration) + + DispatchQueue.main.async { + self.interaction.analysis = nil + self.interaction.preferredInteractionTypes = [] + + self.interaction.analysis = analysis + self.interaction.preferredInteractionTypes = .textSelection + } + } catch { + Console.error(error.localizedDescription) + } + } + } + + + @objc func copyButtonTapped() { + if let theString = UIPasteboard.general.string { + copiedText = theString + Console.log(copiedText) + listener?.copyButtonTapped(with: copiedText) + } + } + + func setImage(with asset: PHAsset) { + imageView.addInteraction(interaction) + + imageView.fetchImage( + asset: asset, + contentMode: .default, + targetSize: imageView.frame.size + ) { [weak self] aspectRatio in + self?.imageView.snp.makeConstraints { + $0.width.equalTo(DeviceSize.width) + $0.height.equalTo(aspectRatio * DeviceSize.width) + } + self?.showLiveText() + } + } +} + +// MARK: - Layout Settting + +private extension SelectedPhotoCell { + + func addSubviews() { + contentView.addSubview(mainScrollView) + mainScrollView.addSubview(imageView) + } + + func setConstraints() { + mainScrollView.snp.makeConstraints { + $0.top.bottom.leading.trailing.equalTo(contentView.safeAreaLayoutGuide) + } + + imageView.snp.makeConstraints { + $0.top.bottom.leading.trailing.equalTo(mainScrollView) + } + } +} diff --git a/Particle/Particle/Main/MainBuilder.swift b/Particle/Particle/Main/MainBuilder.swift index 28f70d1..abeb878 100644 --- a/Particle/Particle/Main/MainBuilder.swift +++ b/Particle/Particle/Main/MainBuilder.swift @@ -7,15 +7,9 @@ import RIBs -protocol MainDependency: Dependency { - var organizingSentenceRepository: OrganizingSentenceRepository { get } -} +protocol MainDependency: Dependency { } -final class MainComponent: Component, OrganizingSentenceDependency, SetAdditionalInformationDependency { - var organizingSentenceRepository: OrganizingSentenceRepository { - dependency.organizingSentenceRepository - } -} +final class MainComponent: Component { } // MARK: - Builder diff --git a/Particle/Particle/Root/RootComponent+LoggedIn.swift b/Particle/Particle/Root/RootComponent+LoggedIn.swift index da0143b..e5ef2d6 100644 --- a/Particle/Particle/Root/RootComponent+LoggedIn.swift +++ b/Particle/Particle/Root/RootComponent+LoggedIn.swift @@ -13,11 +13,7 @@ protocol RootDependencyLoggedIn: Dependency { extension RootComponent: LoggedInDependency { - var LoggedInViewController: LoggedInViewControllable { + var loggedInViewController: LoggedInViewControllable { return rootViewController } - - var organizingSentenceRepository: OrganizingSentenceRepository { - return OrganizingSentenceRepositoryImp() - } } diff --git a/Particle/Particle/Root/RootRouter.swift b/Particle/Particle/Root/RootRouter.swift index 7be03b8..a6cfe5b 100644 --- a/Particle/Particle/Root/RootRouter.swift +++ b/Particle/Particle/Root/RootRouter.swift @@ -58,7 +58,6 @@ final class RootRouter: LaunchRouter, Ro private let loggedInBuilder: LoggedInBuildable private var loggedOut: ViewableRouting? -// private var loggedIn: Routing? private func routeToLoggedOut() { let loggedOut = loggedOutBuilder.build(withListener: interactor)