diff --git a/SOOUM/SOOUM.xcodeproj/project.pbxproj b/SOOUM/SOOUM.xcodeproj/project.pbxproj index ab57272f..2df5275c 100644 --- a/SOOUM/SOOUM.xcodeproj/project.pbxproj +++ b/SOOUM/SOOUM.xcodeproj/project.pbxproj @@ -512,6 +512,8 @@ 389EF81B2D2F454600E053AE /* Log+Extract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389EF8192D2F454600E053AE /* Log+Extract.swift */; }; 389EF81E2D2F469B00E053AE /* CocoaLumberjack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389EF81D2D2F469B00E053AE /* CocoaLumberjack.swift */; }; 389EF81F2D2F469B00E053AE /* CocoaLumberjack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389EF81D2D2F469B00E053AE /* CocoaLumberjack.swift */; }; + 38A27E732F0812BA00DA1D4D /* SearchTextFieldView+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A27E722F0812B200DA1D4D /* SearchTextFieldView+Rx.swift */; }; + 38A27E742F0812BA00DA1D4D /* SearchTextFieldView+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A27E722F0812B200DA1D4D /* SearchTextFieldView+Rx.swift */; }; 38A5D1542C8CB11E00B68363 /* UIImage+SOOUM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A5D1532C8CB11E00B68363 /* UIImage+SOOUM.swift */; }; 38A5D1552C8CB12300B68363 /* UIImage+SOOUM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A5D1532C8CB11E00B68363 /* UIImage+SOOUM.swift */; }; 38A627172CECC5A800C37A03 /* SOMTagsLayoutConfigure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A627162CECC5A800C37A03 /* SOMTagsLayoutConfigure.swift */; }; @@ -780,10 +782,6 @@ 38EBA90F2EB39920008B28F4 /* PostingPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38EBA90D2EB39917008B28F4 /* PostingPermission.swift */; }; 38EBA9112EB399A1008B28F4 /* PostingPermissionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38EBA9102EB3999C008B28F4 /* PostingPermissionResponse.swift */; }; 38EBA9122EB399A1008B28F4 /* PostingPermissionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38EBA9102EB3999C008B28F4 /* PostingPermissionResponse.swift */; }; - 38EC8D002ED44661009C2857 /* TagSearchCollectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38EC8CFF2ED44658009C2857 /* TagSearchCollectViewController.swift */; }; - 38EC8D012ED44661009C2857 /* TagSearchCollectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38EC8CFF2ED44658009C2857 /* TagSearchCollectViewController.swift */; }; - 38EC8D032ED44669009C2857 /* TagSearchCollectViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38EC8D022ED44664009C2857 /* TagSearchCollectViewReactor.swift */; }; - 38EC8D042ED44669009C2857 /* TagSearchCollectViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38EC8D022ED44664009C2857 /* TagSearchCollectViewReactor.swift */; }; 38F161432ECDA858003BADB6 /* SearchViewButton+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F161422ECDA853003BADB6 /* SearchViewButton+Rx.swift */; }; 38F161442ECDA858003BADB6 /* SearchViewButton+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F161422ECDA853003BADB6 /* SearchViewButton+Rx.swift */; }; 38F161472ECDA8F0003BADB6 /* FavoriteTagPlaceholderViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F161462ECDA8D1003BADB6 /* FavoriteTagPlaceholderViewCell.swift */; }; @@ -1113,6 +1111,7 @@ 389EF8162D2F450000E053AE /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; 389EF8192D2F454600E053AE /* Log+Extract.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Log+Extract.swift"; sourceTree = ""; }; 389EF81D2D2F469B00E053AE /* CocoaLumberjack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CocoaLumberjack.swift; sourceTree = ""; }; + 38A27E722F0812B200DA1D4D /* SearchTextFieldView+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchTextFieldView+Rx.swift"; sourceTree = ""; }; 38A5D1532C8CB11E00B68363 /* UIImage+SOOUM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SOOUM.swift"; sourceTree = ""; }; 38A627162CECC5A800C37A03 /* SOMTagsLayoutConfigure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMTagsLayoutConfigure.swift; sourceTree = ""; }; 38A721942E73E7010071E1D8 /* View+SwiftEntryKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+SwiftEntryKit.swift"; sourceTree = ""; }; @@ -1251,8 +1250,6 @@ 38E9CE182D37FED000E85A2D /* AddingTokenInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddingTokenInterceptor.swift; sourceTree = ""; }; 38EBA90D2EB39917008B28F4 /* PostingPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingPermission.swift; sourceTree = ""; }; 38EBA9102EB3999C008B28F4 /* PostingPermissionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingPermissionResponse.swift; sourceTree = ""; }; - 38EC8CFF2ED44658009C2857 /* TagSearchCollectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSearchCollectViewController.swift; sourceTree = ""; }; - 38EC8D022ED44664009C2857 /* TagSearchCollectViewReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSearchCollectViewReactor.swift; sourceTree = ""; }; 38F161422ECDA853003BADB6 /* SearchViewButton+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewButton+Rx.swift"; sourceTree = ""; }; 38F161462ECDA8D1003BADB6 /* FavoriteTagPlaceholderViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteTagPlaceholderViewCell.swift; sourceTree = ""; }; 38F161492ECDAD29003BADB6 /* FavoriteTagViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteTagViewCell.swift; sourceTree = ""; }; @@ -1457,7 +1454,6 @@ 38787B7D2ED1E8EF004BBAA7 /* TagSearchViewReactor.swift */, 38787B732ED1E10E004BBAA7 /* Cells */, 3803B92F2ECF5F15009D14B9 /* Views */, - 38EC8CFE2ED44643009C2857 /* Search+Collect */, ); path = Search; sourceTree = ""; @@ -1486,6 +1482,7 @@ isa = PBXGroup; children = ( 3803B9302ECF5F1B009D14B9 /* SearchTextFieldView.swift */, + 38A27E722F0812B200DA1D4D /* SearchTextFieldView+Rx.swift */, ); path = Views; sourceTree = ""; @@ -2713,15 +2710,6 @@ path = WrittenTags; sourceTree = ""; }; - 38EC8CFE2ED44643009C2857 /* Search+Collect */ = { - isa = PBXGroup; - children = ( - 38EC8CFF2ED44658009C2857 /* TagSearchCollectViewController.swift */, - 38EC8D022ED44664009C2857 /* TagSearchCollectViewReactor.swift */, - ); - path = "Search+Collect"; - sourceTree = ""; - }; 38F161452ECDA8B3003BADB6 /* Cells */ = { isa = PBXGroup; children = ( @@ -3213,7 +3201,6 @@ 2AFD056E2D048CAF007C84AD /* TagRequest.swift in Sources */, 38CA91F42EBDCFF2002C261A /* ProfileCardsPlaceholderViewCell.swift in Sources */, 38E928D02EB75FA300B3F00B /* PungView.swift in Sources */, - 38EC8D042ED44669009C2857 /* TagSearchCollectViewReactor.swift in Sources */, 3879B4B82EC5ADC50070846B /* RejoinableDateInfoResponse.swift in Sources */, 38E928C82EB73FF800B3F00B /* WrittenTagModel.swift in Sources */, 388698602D1984D600008600 /* NotificationViewReactor.swift in Sources */, @@ -3356,7 +3343,6 @@ 38121E322CA6C77500602499 /* Double.swift in Sources */, 380F42252E884AE5009AC59E /* CardRemoteDataSource.swift in Sources */, 381B83EC2EBC769900C84015 /* ProfileCardInfoResponse.swift in Sources */, - 38EC8D012ED44661009C2857 /* TagSearchCollectViewController.swift in Sources */, 38E9CE1A2D37FED000E85A2D /* AddingTokenInterceptor.swift in Sources */, 38F88EBB2D2C1CB8002AD7A8 /* Info.swift in Sources */, 2A5BB7D62CDCA5C900E1C799 /* TermsOfServiceAgreeButtonView.swift in Sources */, @@ -3439,6 +3425,7 @@ 380F42222E87ECA3009AC59E /* CompositeNotificationInfo.swift in Sources */, 38AE85132EDF424800029E4C /* FetchBlockUserUseCaseImpl.swift in Sources */, 38899E5E2E7937E50030F7CA /* NicknameValidateResponse.swift in Sources */, + 38A27E732F0812BA00DA1D4D /* SearchTextFieldView+Rx.swift in Sources */, 38AE851C2EDFF7E000029E4C /* FetchFollowUseCaseImpl.swift in Sources */, 385053532C92DBE200C80B02 /* SOMTabBarItem.swift in Sources */, 38C2A8092EC0BB9800B941A2 /* BlockUserViewCell.swift in Sources */, @@ -3636,7 +3623,6 @@ 380F42312E884FBC009AC59E /* CardRepository.swift in Sources */, 387FA11D2E88DDC1004DF7CE /* HomeViewReactor.swift in Sources */, 3800575C2D9C12CB00E58A19 /* DefinedError.swift in Sources */, - 38EC8D032ED44669009C2857 /* TagSearchCollectViewReactor.swift in Sources */, 38899E932E79518F0030F7CA /* CommonNotificationInfo.swift in Sources */, 3893B6D12D36739500F2004C /* CompositeManager.swift in Sources */, 3836ACB72C8F04CD00A3C566 /* UILabel+Observer.swift in Sources */, @@ -3779,7 +3765,6 @@ 385602B62D2FB18400118530 /* NotificationPlaceholderViewCell.swift in Sources */, 38E9CE192D37FED000E85A2D /* AddingTokenInterceptor.swift in Sources */, 380F42242E884AE5009AC59E /* CardRemoteDataSource.swift in Sources */, - 38EC8D002ED44661009C2857 /* TagSearchCollectViewController.swift in Sources */, 3802BDB82D0AF2F7001256EA /* PushManager+Rx.swift in Sources */, 381B83EB2EBC769900C84015 /* ProfileCardInfoResponse.swift in Sources */, 38E9CE132D37711600E85A2D /* OnboardingViewReactor.swift in Sources */, @@ -3862,6 +3847,7 @@ 3878D0882CFFEF0F00F9522F /* TermsOfServiceTextCellView+Rx.swift in Sources */, 38AE85122EDF424800029E4C /* FetchBlockUserUseCaseImpl.swift in Sources */, 3889A2502E79B3260030F7CA /* UserRemoteDataSource.swift in Sources */, + 38A27E742F0812BA00DA1D4D /* SearchTextFieldView+Rx.swift in Sources */, 38AE851B2EDFF7E000029E4C /* FetchFollowUseCaseImpl.swift in Sources */, 380F42212E87ECA3009AC59E /* CompositeNotificationInfo.swift in Sources */, 38899E5F2E7937E50030F7CA /* NicknameValidateResponse.swift in Sources */, @@ -3967,6 +3953,110 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 2.1.5; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-l\"c++\"", + "-l\"sqlite3\"", + "-l\"swiftCoreGraphics\"", + "-l\"z\"", + "-framework", + "\"Accelerate\"", + "-framework", + "\"Alamofire\"", + "-framework", + "\"CFNetwork\"", + "-framework", + "\"Clarity\"", + "-framework", + "\"CocoaLumberjack\"", + "-framework", + "\"CoreGraphics\"", + "-framework", + "\"CoreTelephony\"", + "-framework", + "\"FBLPromises\"", + "-framework", + "\"FirebaseAnalytics\"", + "-framework", + "\"FirebaseCore\"", + "-framework", + "\"FirebaseCoreExtension\"", + "-framework", + "\"FirebaseCoreInternal\"", + "-framework", + "\"FirebaseCrashlytics\"", + "-framework", + "\"FirebaseInstallations\"", + "-framework", + "\"FirebaseMessaging\"", + "-framework", + "\"FirebaseRemoteConfigInterop\"", + "-framework", + "\"FirebaseSessions\"", + "-framework", + "\"Foundation\"", + "-framework", + "\"GoogleAdsOnDeviceConversion\"", + "-framework", + "\"GoogleAppMeasurement\"", + "-framework", + "\"GoogleAppMeasurementIdentitySupport\"", + "-framework", + "\"GoogleDataTransport\"", + "-framework", + "\"GoogleUtilities\"", + "-framework", + "\"Kingfisher\"", + "-framework", + "\"Lottie\"", + "-framework", + "\"Promises\"", + "-framework", + "\"PryntTrimmerView\"", + "-framework", + "\"QuartzCore\"", + "-framework", + "\"ReactorKit\"", + "-framework", + "\"RxCocoa\"", + "-framework", + "\"RxGesture\"", + "-framework", + "\"RxKeyboard\"", + "-framework", + "\"RxRelay\"", + "-framework", + "\"RxSwift\"", + "-framework", + "\"Security\"", + "-framework", + "\"SnapKit\"", + "-framework", + "\"Stevia\"", + "-framework", + "\"StoreKit\"", + "-framework", + "\"SwiftEntryKit\"", + "-framework", + "\"SystemConfiguration\"", + "-framework", + "\"Then\"", + "-framework", + "\"UIKit\"", + "-framework", + "\"WeakMapTable\"", + "-framework", + "\"YPImagePicker\"", + "-framework", + "\"nanopb\"", + "-weak_framework", + "\"Combine\"", + "-weak_framework", + "\"SwiftUI\"", + "-weak_framework", + "\"UserNotifications\"", + ); OTHER_SWIFT_FLAGS = "$(inherited) -D DEVELOP"; PRODUCT_BUNDLE_IDENTIFIER = com.sooum.dev; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4019,6 +4109,110 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 2.1.5; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-l\"c++\"", + "-l\"sqlite3\"", + "-l\"swiftCoreGraphics\"", + "-l\"z\"", + "-framework", + "\"Accelerate\"", + "-framework", + "\"Alamofire\"", + "-framework", + "\"CFNetwork\"", + "-framework", + "\"Clarity\"", + "-framework", + "\"CocoaLumberjack\"", + "-framework", + "\"CoreGraphics\"", + "-framework", + "\"CoreTelephony\"", + "-framework", + "\"FBLPromises\"", + "-framework", + "\"FirebaseAnalytics\"", + "-framework", + "\"FirebaseCore\"", + "-framework", + "\"FirebaseCoreExtension\"", + "-framework", + "\"FirebaseCoreInternal\"", + "-framework", + "\"FirebaseCrashlytics\"", + "-framework", + "\"FirebaseInstallations\"", + "-framework", + "\"FirebaseMessaging\"", + "-framework", + "\"FirebaseRemoteConfigInterop\"", + "-framework", + "\"FirebaseSessions\"", + "-framework", + "\"Foundation\"", + "-framework", + "\"GoogleAdsOnDeviceConversion\"", + "-framework", + "\"GoogleAppMeasurement\"", + "-framework", + "\"GoogleAppMeasurementIdentitySupport\"", + "-framework", + "\"GoogleDataTransport\"", + "-framework", + "\"GoogleUtilities\"", + "-framework", + "\"Kingfisher\"", + "-framework", + "\"Lottie\"", + "-framework", + "\"Promises\"", + "-framework", + "\"PryntTrimmerView\"", + "-framework", + "\"QuartzCore\"", + "-framework", + "\"ReactorKit\"", + "-framework", + "\"RxCocoa\"", + "-framework", + "\"RxGesture\"", + "-framework", + "\"RxKeyboard\"", + "-framework", + "\"RxRelay\"", + "-framework", + "\"RxSwift\"", + "-framework", + "\"Security\"", + "-framework", + "\"SnapKit\"", + "-framework", + "\"Stevia\"", + "-framework", + "\"StoreKit\"", + "-framework", + "\"SwiftEntryKit\"", + "-framework", + "\"SystemConfiguration\"", + "-framework", + "\"Then\"", + "-framework", + "\"UIKit\"", + "-framework", + "\"WeakMapTable\"", + "-framework", + "\"YPImagePicker\"", + "-framework", + "\"nanopb\"", + "-weak_framework", + "\"Combine\"", + "-weak_framework", + "\"SwiftUI\"", + "-weak_framework", + "\"UserNotifications\"", + ); OTHER_SWIFT_FLAGS = "$(inherited) -D DEVELOP"; PRODUCT_BUNDLE_IDENTIFIER = com.sooum.dev; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/SOOUM/SOOUM/Extensions/Cocoa/UITextField.swift b/SOOUM/SOOUM/Extensions/Cocoa/UITextField.swift index 31c3b716..da22b9b1 100644 --- a/SOOUM/SOOUM/Extensions/Cocoa/UITextField.swift +++ b/SOOUM/SOOUM/Extensions/Cocoa/UITextField.swift @@ -9,14 +9,23 @@ import UIKit extension UITextField { - func shouldChangeCharactersIn(in range: NSRange, replacementString string: String, maxCharacters limit: Int) -> Bool { + func shouldChangeCharactersIn( + in range: NSRange, + replacementString string: String, + maxCharacters limit: Int, + // 공백 제거 여부 + hasSpaces: Bool = true + ) -> Bool { guard let text = self.text else { return true } if string.isEmpty { return true } - + + var removedString: String { + return hasSpaces ? string : string.replacingOccurrences(of: " ", with: "") + } let nsString: NSString? = text as NSString? let newString: String = nsString?.replacingCharacters(in: range, with: string) ?? "" @@ -36,9 +45,9 @@ extension UITextField { let separatedCharactersCount = separatedCharacters.count // 마지막 문자를 자음 + 모음으로 나누어 갯수에 따라 판단, // 갯수가 1일 때, 모음이면 입력 가능 - if separatedCharactersCount == 1 && lastCharacter.isConsonant && string.isConsonant == false { return true } + if separatedCharactersCount == 1 && lastCharacter.isConsonant && removedString.isConsonant == false { return true } // 갯수가 2일 때, 자음이면 입력 가능 - if separatedCharactersCount == 2 && string.isConsonant { return true } + if separatedCharactersCount == 2 && removedString.isConsonant { return true } // TODO: 겹받침일 때는 고려 X return false @@ -46,7 +55,7 @@ extension UITextField { // 텍스트 범위가 선택됨 // 추가되는 문자열에서 선택된 범위의 길이만큼만 교체 let to: Int = range.length - let validText: String = String(string.prefix(max(0, to))) + let validText: String = String(removedString.prefix(max(0, to))) self.text = nsString?.replacingCharacters(in: range, with: validText) if let position = self.position(from: self.beginningOfDocument, offset: range.location + to) { @@ -60,7 +69,7 @@ extension UITextField { // 텍스트 입력 후에 제한을 벗어남 // 추가되는 문자열에서 제한을 넘지 않는 길이만큼만 추가 let to: Int = limit - text.count - let validText: String = String(string.prefix(max(0, to))) + let validText: String = String(removedString.prefix(max(0, to))) self.text = nsString?.replacingCharacters(in: range, with: validText) if let position = self.position(from: self.beginningOfDocument, offset: range.location + to) { @@ -72,7 +81,20 @@ extension UITextField { } return false } else { - return true + if hasSpaces { + return true + } else { + let newPositionOffset = range.location + removedString.count + self.text = nsString?.replacingCharacters(in: range, with: removedString) + + if let newPosition = self.position(from: self.beginningOfDocument, offset: newPositionOffset) { + DispatchQueue.main.async { [weak self] in + self?.selectedTextRange = self?.textRange(from: newPosition, to: newPosition) + } + } + self.sendActions(for: .editingChanged) + return false + } } } } diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Cells/HomePlaceholderViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Home/Cells/HomePlaceholderViewCell.swift index 5e3f7a9e..5d48eff2 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Cells/HomePlaceholderViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Cells/HomePlaceholderViewCell.swift @@ -12,10 +12,6 @@ import Then class HomePlaceholderViewCell: UITableViewCell { - enum Text { - static let message: String = "아직 작성된 글이 없어요\n하고 싶은 이야기를 카드로 남겨보세요" - } - static let cellIdentifier = String(reflecting: HomePlaceholderViewCell.self) @@ -26,10 +22,12 @@ class HomePlaceholderViewCell: UITableViewCell { $0.contentMode = .scaleAspectFit } - private let placeholderMessageLabel = UILabel().then { - $0.text = Text.message + private let placeholderLabel = UILabel().then { $0.textColor = .som.v2.gray400 $0.typography = .som.v2.body1 + $0.numberOfLines = 0 + $0.lineBreakMode = .byWordWrapping + $0.lineBreakStrategy = .hangulWordPriority } @@ -62,10 +60,18 @@ class HomePlaceholderViewCell: UITableViewCell { $0.height.equalTo(113) } - self.contentView.addSubview(self.placeholderMessageLabel) - self.placeholderMessageLabel.snp.makeConstraints { + self.contentView.addSubview(self.placeholderLabel) + self.placeholderLabel.snp.makeConstraints { $0.top.equalTo(self.placeholderImageView.snp.bottom).offset(20) $0.centerX.equalToSuperview() } } + + + // MARK: Public info + + func bind(_ placeholderText: String) { + self.placeholderLabel.text = placeholderText + self.placeholderLabel.typography = .som.v2.body1 + } } diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewCell.swift index 99d304c2..362d4286 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewCell.swift @@ -226,19 +226,23 @@ class DetailViewCell: UICollectionViewCell { private func updateTextContainerInsetAndHeight(_ content: String, typography: Typography) { - var attributes = typography.attributes - attributes[.font] = typography.font - let attributedText = NSAttributedString( - string: content, - attributes: attributes - ) - + // UILabel의 정확한 높이를 구하기 위해 sizeToFit 사용 let size: CGSize = .init(width: self.contentScrollView.bounds.width, height: .greatestFiniteMagnitude) - let boundingHeight = attributedText.boundingRect( - with: size, - options: [.usesLineFragmentOrigin], - context: nil - ).height + var boundingHeight: CGFloat { + let label = UILabel(frame: .init(origin: .zero, size: size)).then { + $0.text = content + $0.textColor = .som.v2.white + $0.typography = .som.v2.body1 + $0.textAlignment = .center + $0.numberOfLines = 0 + $0.lineBreakMode = .byWordWrapping + $0.lineBreakStrategy = .hangulWordPriority + + $0.sizeToFit() + } + + return label.frame.height + } let lines: CGFloat = boundingHeight / typography.lineHeight let isScrollEnabled: Bool = lines > 8 diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Views/MemberInfoView.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Views/MemberInfoView.swift index b0a593e7..20312711 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Views/MemberInfoView.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Views/MemberInfoView.swift @@ -153,7 +153,7 @@ class MemberInfoView: UIView { self.distanceLabel.snp.makeConstraints { $0.centerY.equalToSuperview() $0.leading.equalTo(self.distanceImageView.snp.trailing).offset(2) - $0.trailing.equalToSuperview().offset(-4) + $0.trailing.equalToSuperview().offset(-6) } self.addSubview(self.timeGapLabel) diff --git a/SOOUM/SOOUM/Presentations/Main/Home/HomeViewController.swift b/SOOUM/SOOUM/Presentations/Main/Home/HomeViewController.swift index da162222..98a5533e 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/HomeViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/HomeViewController.swift @@ -27,6 +27,9 @@ class HomeViewController: BaseNavigationViewController, View { static let distanceFilternder20km: String = "20km" static let distanceFilternder50km: String = "50km" + static let defaultPlaceholderText: String = "아직 작성된 글이 없어요\n하고 싶은 이야기를 카드로 남겨보세요" + static let distancePlaceholderText: String = "아직 주변에서 작성된 카드가 없어요\n탭을 눌러 거리를 넓히거나, 첫 번째 카드를 남겨보세요" + static let dialogTitle: String = "위치 정보 사용 설정" static let dialogMessage: String = "내 위치 확인을 위해 ‘설정 > 앱 > 숨 > 위치’에서 위치 정보 사용을 허용해 주세요." @@ -50,7 +53,7 @@ class HomeViewController: BaseNavigationViewController, View { case latest(BaseCardInfo) case popular(BaseCardInfo) case distance(BaseCardInfo) - case empty + case empty(String) } @@ -149,9 +152,12 @@ class HomeViewController: BaseNavigationViewController, View { cell.bind(cardInfo) return cell - case .empty: + case let .empty(placeholderText): + + let placeholder = self.cellForPlaceholder(tableView, with: indexPath) + placeholder.bind(placeholderText) - return self.cellForPlaceholder(tableView, with: indexPath) + return placeholder } } @@ -424,7 +430,7 @@ class HomeViewController: BaseNavigationViewController, View { guard let latests = displayStats.latests else { return } guard latests.isEmpty == false else { - snapshot.appendItems([.empty], toSection: .empty) + snapshot.appendItems([.empty(Text.defaultPlaceholderText)], toSection: .empty) break } @@ -435,7 +441,7 @@ class HomeViewController: BaseNavigationViewController, View { guard let populars = displayStats.populars else { return } guard populars.isEmpty == false else { - snapshot.appendItems([.empty], toSection: .empty) + snapshot.appendItems([.empty(Text.defaultPlaceholderText)], toSection: .empty) break } @@ -446,7 +452,7 @@ class HomeViewController: BaseNavigationViewController, View { guard let distances = displayStats.distances else { return } guard distances.isEmpty == false else { - snapshot.appendItems([.empty], toSection: .empty) + snapshot.appendItems([.empty(Text.distancePlaceholderText)], toSection: .empty) break } diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotificationPlaceholderViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotificationPlaceholderViewCell.swift index ab3cc695..0cd00824 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotificationPlaceholderViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotificationPlaceholderViewCell.swift @@ -14,7 +14,7 @@ import Then class NotificationPlaceholderViewCell: UITableViewCell { enum Text { - static let placeholderLabelText: String = "아직 표시할 알림이 없어요" + static let placeholderLabelText: String = "아직 표시할 알림이 없어요\n활동 알림은 30일 후, 자동 삭제돼요" } static let cellIdentifier = String(reflecting: NotificationPlaceholderViewCell.self) @@ -31,6 +31,9 @@ class NotificationPlaceholderViewCell: UITableViewCell { $0.text = Text.placeholderLabelText $0.textColor = .som.v2.gray400 $0.typography = .som.v2.body1 + $0.numberOfLines = 0 + $0.lineBreakMode = .byWordWrapping + $0.lineBreakStrategy = .hangulWordPriority } @@ -61,7 +64,6 @@ class NotificationPlaceholderViewCell: UITableViewCell { } self.contentView.addSubview(self.placeholderLabel) - self.placeholderLabel.snp.makeConstraints { $0.top.equalTo(self.placeholderImage.snp.bottom).offset(20) $0.bottom.equalToSuperview() diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/ProfileViewController.swift b/SOOUM/SOOUM/Presentations/Main/Profile/ProfileViewController.swift index 1095c154..d8820ca5 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/ProfileViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/ProfileViewController.swift @@ -361,9 +361,10 @@ class ProfileViewController: BaseNavigationViewController, View { } ) + let nickname = reactor.currentState.profileInfo?.nickname ?? "" SOMDialogViewController.show( title: Text.blockDialogTitle, - message: Text.blockDialogMessage, + message: nickname + Text.blockDialogMessage, textAlignment: .left, actions: [cancelAction, confirmAction] ) diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/Collect/Cells/TagCollectCardsView.swift b/SOOUM/SOOUM/Presentations/Main/Tags/Collect/Cells/TagCollectCardsView.swift index 4725e476..c0abbf78 100644 --- a/SOOUM/SOOUM/Presentations/Main/Tags/Collect/Cells/TagCollectCardsView.swift +++ b/SOOUM/SOOUM/Presentations/Main/Tags/Collect/Cells/TagCollectCardsView.swift @@ -107,6 +107,7 @@ class TagCollectCardsView: UIView { private(set) var models: [ProfileCardInfo]? let cardDidTapped = PublishRelay() + let didScrolled = PublishRelay() let moreFindWithId = PublishRelay() @@ -221,6 +222,8 @@ extension TagCollectCardsView: UICollectionViewDelegateFlowLayout { func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.didScrolled.accept(()) + let offset = scrollView.contentOffset.y // 당겨서 새로고침 diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/Search/Search+Collect/TagSearchCollectViewController.swift b/SOOUM/SOOUM/Presentations/Main/Tags/Search/Search+Collect/TagSearchCollectViewController.swift deleted file mode 100644 index 24341a3b..00000000 --- a/SOOUM/SOOUM/Presentations/Main/Tags/Search/Search+Collect/TagSearchCollectViewController.swift +++ /dev/null @@ -1,274 +0,0 @@ -// -// TagSearchCollectViewController.swift -// SOOUM -// -// Created by 오현식 on 11/24/25. -// - -import UIKit - -import SnapKit -import Then - -import SwiftEntryKit - -import ReactorKit -import RxCocoa -import RxSwift - -class TagSearchCollectViewController: BaseNavigationViewController, View { - - enum Text { - static let bottomToastEntryName: String = "bottomToastEntryName" - static let addToastMessage: String = "을 관심 태그에 추가했어요" - static let deleteToastMessage: String = "을 관심 태그에서 삭제했어요" - - static let bottomToastEntryNameWithAction: String = "bottomToastEntryNameWithAction" - static let failedToastMessage: String = "네트워크 확인 후 재시도해주세요." - static let failToastActionTitle: String = "재시도" - - static let bottomToastEntryNameWithoutAction: String = "bottomToastEntryNameWithoutAction" - static let addAdditionalLimitedFloatMessage: String = "관심 태그는 9개까지 추가할 수 있어요" - - static let pungedCardDialogTitle: String = "삭제된 카드예요" - static let confirmActionTitle: String = "확인" - } - - - // MARK: Views - - private let searchViewButtonView = SearchViewButton() - - private let rightFavoriteButton = SOMButton().then { - $0.image = .init(.icon(.v2(.filled(.star)))) - $0.foregroundColor = .som.v2.gray200 - } - - private let tagCollectCardsView = TagCollectCardsView() - - - // MARK: Override func - - override func setupNaviBar() { - super.setupNaviBar() - - self.searchViewButtonView.snp.makeConstraints { - let leftAndRightButtonsWidth: CGFloat = 24 * 2 - let leftAndRightPadding: CGFloat = 12 * 2 - let width = UIScreen.main.bounds.width - leftAndRightButtonsWidth - leftAndRightPadding - $0.width.equalTo(width) - $0.height.equalTo(44) - } - self.rightFavoriteButton.snp.makeConstraints { - $0.size.equalTo(24) - } - - self.navigationBar.titleView = self.searchViewButtonView - - self.navigationBar.setLeftButtons([]) - self.navigationBar.setRightButtons([self.rightFavoriteButton]) - } - - override func setupConstraints() { - super.setupConstraints() - - self.view.addSubview(self.tagCollectCardsView) - self.tagCollectCardsView.snp.makeConstraints { - $0.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(8) - $0.bottom.horizontalEdges.equalToSuperview() - } - } - - - // MARK: ReactorKit - bind - - func bind(reactor: TagSearchCollectViewReactor) { - - // 상세 화면 전환 - self.tagCollectCardsView.cardDidTapped - .map(\.id) - .throttle(.seconds(3), scheduler: MainScheduler.instance) - .map(Reactor.Action.hasDetailCard) - .bind(to: reactor.action) - .disposed(by: self.disposeBag) - - let viewDidLoad = self.rx.viewDidLoad - // 네비게이션 타이틀 - viewDidLoad - .subscribe(with: self) { object, _ in - object.searchViewButtonView.placeholder = reactor.title - } - .disposed(by: self.disposeBag) - - // Action - viewDidLoad - .map { _ in Reactor.Action.landing } - .bind(to: reactor.action) - .disposed(by: self.disposeBag) - - self.tagCollectCardsView.moreFindWithId - .map(Reactor.Action.more) - .bind(to: reactor.action) - .disposed(by: self.disposeBag) - - let isFavorite = reactor.state.map(\.isFavorite).share() - self.rightFavoriteButton.rx.throttleTap - .withLatestFrom(isFavorite) - .map(Reactor.Action.updateIsFavorite) - .bind(to: reactor.action) - .disposed(by: self.disposeBag) - - let isRefreshing = reactor.state.map(\.isRefreshing).distinctUntilChanged().share() - self.tagCollectCardsView.refreshControl.rx.controlEvent(.valueChanged) - .withLatestFrom(isRefreshing) - .filter { $0 == false } - .delay(.milliseconds(1000), scheduler: MainScheduler.instance) - .map { _ in Reactor.Action.refresh } - .bind(to: reactor.action) - .disposed(by: self.disposeBag) - - // State - isRefreshing - .observe(on: MainScheduler.asyncInstance) - .filter { $0 == false } - .subscribe(with: self.tagCollectCardsView) { tagCollectCardsView, _ in - tagCollectCardsView.isRefreshing = false - } - .disposed(by: self.disposeBag) - - isFavorite - .observe(on: MainScheduler.asyncInstance) - .subscribe(with: self) { object, isFavorite in - object.rightFavoriteButton.foregroundColor = isFavorite ? .som.v2.yMain : .som.v2.gray200 - } - .disposed(by: self.disposeBag) - - let isUpdated = reactor.state.map(\.isUpdated).distinctUntilChanged().filterNil() - isUpdated - .filter { $0 } - .withLatestFrom(isFavorite) - .observe(on: MainScheduler.instance) - .subscribe(with: self) { object, isFavorite in - - if isFavorite { - GAHelper.shared.logEvent(event: GAEvent.TagView.favoriteTagRegister_btnClick) - } - - let message = isFavorite ? Text.addToastMessage : Text.deleteToastMessage - let bottomToastView = SOMBottomToastView( - title: "‘\(reactor.title)’" + message, - actions: nil - ) - - var wrapper: SwiftEntryKitViewWrapper = bottomToastView.sek - wrapper.entryName = Text.bottomToastEntryName - wrapper.showBottomToast(verticalOffset: 34 + 8) - } - .disposed(by: self.disposeBag) - - isUpdated - .filter { $0 == false } - .withLatestFrom(isFavorite) - .observe(on: MainScheduler.instance) - .subscribe(with: self) { object, isFavorite in - - let actions = [ - SOMBottomToastView.ToastAction(title: Text.failToastActionTitle, action: { - SwiftEntryKit.dismiss(.specific(entryName: Text.bottomToastEntryNameWithAction)) { - reactor.action.onNext(.updateIsFavorite(isFavorite)) - } - }) - ] - let bottomToastView = SOMBottomToastView(title: Text.failedToastMessage, actions: actions) - - var wrapper: SwiftEntryKitViewWrapper = bottomToastView.sek - wrapper.entryName = Text.bottomToastEntryNameWithAction - wrapper.showBottomToast(verticalOffset: 34 + 8) - } - .disposed(by: self.disposeBag) - - reactor.state.map(\.hasErrors) - .distinctUntilChanged() - .filterNil() - .filter { $0 } - .observe(on: MainScheduler.instance) - .subscribe(with: self) { object, _ in - - let bottomToastView = SOMBottomToastView(title: Text.addAdditionalLimitedFloatMessage, actions: nil) - - var wrapper: SwiftEntryKitViewWrapper = bottomToastView.sek - wrapper.entryName = Text.bottomToastEntryNameWithoutAction - wrapper.showBottomToast(verticalOffset: 34 + 8) - } - .disposed(by: self.disposeBag) - - reactor.state.map(\.tagCardInfos) - .distinctUntilChanged() - .observe(on: MainScheduler.asyncInstance) - .subscribe(with: self) { object, tagCardInfos in - - object.tagCollectCardsView.setModels(tagCardInfos) - } - .disposed(by: self.disposeBag) - - let cardIsDeleted = reactor.state.map(\.cardIsDeleted) - .distinctUntilChanged(reactor.canPushToDetail) - .filterNil() - cardIsDeleted - .filter { $0.isDeleted } - .map { $0.selectedId } - .observe(on: MainScheduler.instance) - .subscribe(with: self) { object, selectedId in - object.showPungedCardDialog(reactor, with: selectedId) - } - .disposed(by: self.disposeBag) - cardIsDeleted - .filter { $0.isDeleted == false } - .map { $0.selectedId } - .do(onNext: { _ in - reactor.action.onNext(.cleanup) - - GAHelper.shared.logEvent( - event: GAEvent.DetailView.cardDetail_tracePathClick( - previous_path: .tag_search_collect - ) - ) - }) - .observe(on: MainScheduler.instance) - .subscribe(with: self) { object, selectedId in - let detailViewController = DetailViewController() - detailViewController.reactor = reactor.reactorForDetail(with: selectedId) - object.navigationPush(detailViewController, animated: true) - } - .disposed(by: self.disposeBag) - } -} - -private extension TagSearchCollectViewController { - - func showPungedCardDialog(_ reactor: TagSearchCollectViewReactor, with selectedId: String) { - - let confirmAction = SOMDialogAction( - title: Text.confirmActionTitle, - style: .primary, - action: { - SOMDialogViewController.dismiss { - reactor.action.onNext(.cleanup) - - reactor.action.onNext( - .updateTagCards( - reactor.currentState.tagCardInfos.filter { $0.id != selectedId } - ) - ) - } - } - ) - - SOMDialogViewController.show( - title: Text.pungedCardDialogTitle, - messageView: nil, - textAlignment: .left, - actions: [confirmAction] - ) - } -} diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/Search/Search+Collect/TagSearchCollectViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Tags/Search/Search+Collect/TagSearchCollectViewReactor.swift deleted file mode 100644 index 5be7ac67..00000000 --- a/SOOUM/SOOUM/Presentations/Main/Tags/Search/Search+Collect/TagSearchCollectViewReactor.swift +++ /dev/null @@ -1,202 +0,0 @@ -// -// TagSearchCollectViewReactor.swift -// SOOUM -// -// Created by 오현식 on 11/24/25. -// - -import ReactorKit - -class TagSearchCollectViewReactor: Reactor { - - enum Action: Equatable { - case landing - case refresh - case more(String) - case updateTagCards([ProfileCardInfo]) - case hasDetailCard(String) - case updateIsFavorite(Bool) - case cleanup - } - - enum Mutation { - case tagCardInfos([ProfileCardInfo]) - case moreFind([ProfileCardInfo]) - case cardIsDeleted((String, Bool)?) - case updateIsUpdate(Bool?) - case updateIsFavorite(Bool) - case updateIsRefreshing(Bool) - case updateHasErrors(Bool?) - } - - struct State { - fileprivate(set) var tagCardInfos: [ProfileCardInfo] - fileprivate(set) var cardIsDeleted: (selectedId: String, isDeleted: Bool)? - fileprivate(set) var isUpdated: Bool? - fileprivate(set) var isFavorite: Bool - fileprivate(set) var isRefreshing: Bool - fileprivate(set) var hasErrors: Bool? - } - - var initialState: State = .init( - tagCardInfos: [], - cardIsDeleted: nil, - isUpdated: nil, - isFavorite: false, - isRefreshing: false, - hasErrors: nil - ) - - private let dependencies: AppDIContainerable - private let fetchCardUseCase: FetchCardUseCase - private let fetchCardDetailUseCase: FetchCardDetailUseCase - private let fetchTagUseCase: FetchTagUseCase - private let updateTagFavoriteUseCase: UpdateTagFavoriteUseCase - - private let tagId: String - let title: String - - init(dependencies: AppDIContainerable, with tagId: String, title: String) { - self.dependencies = dependencies - self.fetchCardUseCase = dependencies.rootContainer.resolve(FetchCardUseCase.self) - self.fetchCardDetailUseCase = dependencies.rootContainer.resolve(FetchCardDetailUseCase.self) - self.fetchTagUseCase = dependencies.rootContainer.resolve(FetchTagUseCase.self) - self.updateTagFavoriteUseCase = dependencies.rootContainer.resolve(UpdateTagFavoriteUseCase.self) - - self.tagId = tagId - self.title = title - } - - func mutate(action: Action) -> Observable { - switch action { - case .landing: - - return self.cardsWithTag(with: nil) - .catchAndReturn(.tagCardInfos([])) - case .refresh: - - return .concat([ - .just(.updateIsRefreshing(true)), - self.cardsWithTag(with: nil) - .catchAndReturn(.tagCardInfos([])), - .just(.updateIsRefreshing(false)) - ]) - case let .more(lastId): - - return self.fetchCardUseCase.cardsWithTag(tagId: self.tagId, lastId: lastId) - .map(\.cardInfos) - .map(Mutation.moreFind) - case let .updateTagCards(tagCardInfos): - - return .just(.tagCardInfos(tagCardInfos)) - case let .hasDetailCard(selectedId): - - return .concat([ - .just(.cardIsDeleted(nil)), - self.fetchCardDetailUseCase.isDeleted(cardId: selectedId) - .map { (selectedId, $0) } - .map(Mutation.cardIsDeleted) - ]) - case let .updateIsFavorite(isFavorite): - - return .concat([ - .just(.updateIsUpdate(nil)), - .just(.updateHasErrors(nil)), - self.updateTagFavoriteUseCase.updateFavorite(tagId: self.tagId, isFavorite: !isFavorite) - .flatMapLatest { isUpdated -> Observable in - - let isFavorite = isUpdated ? !isFavorite : isFavorite - return .concat([ - .just(.updateIsFavorite(isFavorite)), - .just(.updateIsUpdate(isUpdated)) - ]) - } - .catch(self.catchClosure) - ]) - case .cleanup: - - return .just(.cardIsDeleted(nil)) - } - } - - func reduce(state: State, mutation: Mutation) -> State { - var newState: State = state - switch mutation { - case let .tagCardInfos(tagCardInfos): - newState.tagCardInfos = tagCardInfos - case let .moreFind(tagCardInfos): - newState.tagCardInfos += tagCardInfos - case let .cardIsDeleted(cardIsDeleted): - newState.cardIsDeleted = cardIsDeleted - case let .updateIsUpdate(isUpdated): - newState.isUpdated = isUpdated - case let .updateIsFavorite(isFavorite): - newState.isFavorite = isFavorite - case let .updateIsRefreshing(isRefreshing): - newState.isRefreshing = isRefreshing - case let .updateHasErrors(hasErrors): - newState.hasErrors = hasErrors - } - return newState - } -} - -private extension TagSearchCollectViewReactor { - - func cardsWithTag(with lastId: String?) -> Observable { - - return self.fetchCardUseCase.cardsWithTag(tagId: self.tagId, lastId: nil) - .withUnretained(self) - .flatMapLatest { object, tagCardsInfo -> Observable in - - if tagCardsInfo.cardInfos.isEmpty { - let tagInfo = FavoriteTagInfo(id: object.tagId, title: object.title) - return object.fetchTagUseCase.isFavorites(with: tagInfo) - .flatMapLatest { isFavorite -> Observable in - - return .concat([ - .just(.tagCardInfos(tagCardsInfo.cardInfos)), - .just(.updateIsFavorite(isFavorite)) - ]) - } - } - - return .concat([ - .just(.tagCardInfos(tagCardsInfo.cardInfos)), - .just(.updateIsFavorite(tagCardsInfo.isFavorite)) - ]) - } - } - - var catchClosure: ((Error) throws -> Observable) { - return { error in - - let nsError = error as NSError - - if case 400 = nsError.code { - - return .just(.updateHasErrors(true)) - } - - return .just(.updateIsUpdate(false)) - } - } -} - -extension TagSearchCollectViewReactor { - - func canPushToDetail( - prev prevCardIsDeleted: (selectedId: String, isDeleted: Bool)?, - curr currCardIsDeleted: (selectedId: String, isDeleted: Bool)? - ) -> Bool { - return prevCardIsDeleted?.selectedId == currCardIsDeleted?.selectedId && - prevCardIsDeleted?.isDeleted == currCardIsDeleted?.isDeleted - } -} - -extension TagSearchCollectViewReactor { - - func reactorForDetail(with id: String) -> DetailViewReactor { - DetailViewReactor(dependencies: self.dependencies, with: id) - } -} diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/Search/TagSearchViewController.swift b/SOOUM/SOOUM/Presentations/Main/Tags/Search/TagSearchViewController.swift index 97f8c5ec..a11f8e3d 100644 --- a/SOOUM/SOOUM/Presentations/Main/Tags/Search/TagSearchViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Tags/Search/TagSearchViewController.swift @@ -10,6 +10,8 @@ import UIKit import SnapKit import Then +import SwiftEntryKit + import ReactorKit import RxCocoa import RxSwift @@ -18,6 +20,20 @@ class TagSearchViewController: BaseNavigationViewController, View { enum Text { static let placeholderText: String = "태그를 검색하세요" + + static let bottomToastEntryName: String = "bottomToastEntryName" + static let addToastMessage: String = "을 관심 태그에 추가했어요" + static let deleteToastMessage: String = "을 관심 태그에서 삭제했어요" + + static let bottomToastEntryNameWithAction: String = "bottomToastEntryNameWithAction" + static let failedToastMessage: String = "네트워크 확인 후 재시도해주세요." + static let failToastActionTitle: String = "재시도" + + static let bottomToastEntryNameWithoutAction: String = "bottomToastEntryNameWithoutAction" + static let addAdditionalLimitedFloatMessage: String = "관심 태그는 9개까지 추가할 수 있어요" + + static let pungedCardDialogTitle: String = "삭제된 카드예요" + static let confirmActionTitle: String = "확인" } @@ -27,10 +43,24 @@ class TagSearchViewController: BaseNavigationViewController, View { $0.placeholder = Text.placeholderText } + private let rightFavoriteButton = SOMButton().then { + $0.image = .init(.icon(.v2(.filled(.star)))) + $0.foregroundColor = .som.v2.gray200 + } + private let searchTermsView = SearchTermsView().then { $0.isHidden = true } + private let tagCollectCardsView = TagCollectCardsView().then { + $0.isHidden = true + } + + + // MARK: Constraints + + private var searchTextFieldWidthConstraint: Constraint? + // MARK: Override func @@ -39,15 +69,27 @@ class TagSearchViewController: BaseNavigationViewController, View { override func setupNaviBar() { super.setupNaviBar() + var width: CGFloat + if self.reactor?.currentState.tagCardInfos == nil { + width = (UIScreen.main.bounds.width - 16 * 2) - 24 - 12 + self.navigationBar.setRightButtons([]) + } else { + width = UIScreen.main.bounds.width - 24 * 2 - 12 * 2 + self.navigationBar.setRightButtons([self.rightFavoriteButton]) + } + self.searchTextFieldView.snp.makeConstraints { - let width = (UIScreen.main.bounds.width - 16 * 2) - 24 - 12 - $0.width.equalTo(width) + self.searchTextFieldWidthConstraint = $0.width.equalTo(width).constraint $0.height.equalTo(44) } self.navigationBar.titleView = self.searchTextFieldView self.navigationBar.titlePosition = .left - self.navigationBar.setRightButtons([]) + self.rightFavoriteButton.snp.makeConstraints { + $0.size.equalTo(24) + } + + self.navigationBar.setLeftButtons([]) } override func setupConstraints() { @@ -58,6 +100,12 @@ class TagSearchViewController: BaseNavigationViewController, View { $0.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(8) $0.bottom.horizontalEdges.equalToSuperview() } + + self.view.addSubview(self.tagCollectCardsView) + self.tagCollectCardsView.snp.makeConstraints { + $0.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(8) + $0.bottom.horizontalEdges.equalToSuperview() + } } /// 기본 뒤로가기 기능 제거 override func bind() { } @@ -67,8 +115,14 @@ class TagSearchViewController: BaseNavigationViewController, View { func bind(reactor: TagSearchViewReactor) { - // 검색 화면 진입 시 포커스 + // 검색 화면 진입 및 태그 관련 카드 정보가 없을 때, 검색 필드 포커스 + let tagCardInfos = reactor.state.map(\.tagCardInfos).share() self.rx.viewDidAppear + .withLatestFrom( + tagCardInfos, + resultSelector: { $0 && ($1?.isEmpty ?? true) } + ) + .filter { $0 } .subscribe(with: self) { object, _ in object.searchTextFieldView.becomeFirstResponder() } @@ -77,11 +131,17 @@ class TagSearchViewController: BaseNavigationViewController, View { let searchTerms = reactor.state.map(\.searchTerms).share() /// 뒤로가기로 시 TagViewController를 표시할 때, 관심 태그만 리로드 및 검색 초기화 self.navigationBar.backButton.rx.throttleTap - .withLatestFrom(searchTerms) - .map { $0 == nil } - .subscribe(with: self) { object, isNil in + .withLatestFrom(Observable.combineLatest(searchTerms, tagCardInfos)) + .map { ($0 == nil, $1 == nil) } + .subscribe(with: self) { object, combined in + let width = (UIScreen.main.bounds.width - 16 * 2) - 24 - 12 + object.searchTextFieldWidthConstraint?.update(offset: width) + + object.navigationBar.setRightButtons([]) + + let (isSearchTermsNil, isTagCardInfosNil) = combined // 검색 결과가 없을 때만 - if isNil { + if isSearchTermsNil && isTagCardInfosNil { /// 뒤로가기로 TagViewController를 표시할 때, 관심 태그만 리로드 NotificationCenter.default.post( name: .reloadFavoriteTagData, @@ -90,80 +150,268 @@ class TagSearchViewController: BaseNavigationViewController, View { ) object.navigationPop() } else { - reactor.action.onNext(.reset) object.searchTextFieldView.text = nil - object.searchTextFieldView.resignFirstResponder() + + reactor.action.onNext(.cleanup(.search)) + reactor.action.onNext(.cleanup(.tagCard)) } } .disposed(by: self.disposeBag) - // 태그 카드 모아보기 화면 전환 + // 검색 필드에 포커스 됐을 때, 네비게이션 바 및 태그 모아보기 초기화 + self.searchTextFieldView.textField.rx.controlEvent(.editingDidBegin) + .do(onNext: { _ in reactor.action.onNext(.cleanup(.tagCard)) }) + .subscribe(with: self) { object, _ in + object.searchTermsView.isHidden = false + object.tagCollectCardsView.isHidden = true + + let width = (UIScreen.main.bounds.width - 16 * 2) - 24 - 12 + object.searchTextFieldWidthConstraint?.update(offset: width) + + object.navigationBar.setRightButtons([]) + } + .disposed(by: self.disposeBag) + + // 태그 카드 모아보기 전환 self.searchTermsView.backgroundDidTap .throttle(.seconds(3), scheduler: MainScheduler.instance) + .do(onNext: { model in + reactor.action.onNext(.cardsWithTag(.init(id: model.id, title: model.name))) + }) .subscribe(with: self) { object, model in - object.searchTextFieldView.text = nil - reactor.action.onNext(.reset) + object.view.endEditing(true) - let tagSearchCollectViewController = TagSearchCollectViewController() - tagSearchCollectViewController.reactor = reactor.reactorForSearchCollect( - with: model.id, - title: model.name - ) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak object] in - object?.navigationPush( - tagSearchCollectViewController, - animated: true - ) - } + object.searchTermsView.isHidden = true + object.tagCollectCardsView.isHidden = false + + object.searchTextFieldView.text = model.name + + let leftAndRightButtonsWidth: CGFloat = 24 * 2 + let leftAndRightPadding: CGFloat = 12 * 2 + let width = UIScreen.main.bounds.width - leftAndRightButtonsWidth - leftAndRightPadding + object.searchTextFieldWidthConstraint?.update(offset: width) + + object.navigationBar.setRightButtons([object.rightFavoriteButton]) } .disposed(by: self.disposeBag) + // 상세 화면 전환 + self.tagCollectCardsView.cardDidTapped + .map(\.id) + .throttle(.seconds(3), scheduler: MainScheduler.instance) + .map(Reactor.Action.hasDetailCard) + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + // 스크롤 시 키보드 내림 - self.searchTermsView.didScrolled.asObservable() + Observable.merge( + self.searchTermsView.didScrolled.asObservable(), + self.tagCollectCardsView.didScrolled.asObservable() + ) .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in object.view.endEditing(true) } .disposed(by: self.disposeBag) // Action - let searchText = self.searchTextFieldView.textField.rx.text.orEmpty.distinctUntilChanged().share() - // 태그 검색 - searchText + self.searchTextFieldView.rx.text .skip(1) + .filterNil() + .distinctUntilChanged() .debounce(.milliseconds(500), scheduler: MainScheduler.instance) .map(Reactor.Action.search) .bind(to: reactor.action) .disposed(by: self.disposeBag) - // State - searchTerms - .filter { $0 == nil } - .observe(on: MainScheduler.asyncInstance) - .subscribe(with: self) { object, _ in - object.searchTermsView.isHidden = true - } + self.tagCollectCardsView.moreFindWithId + .map(Reactor.Action.more) + .bind(to: reactor.action) .disposed(by: self.disposeBag) - searchTerms - .filterNil() + let isRefreshing = reactor.state.map(\.isRefreshing).distinctUntilChanged().share() + self.tagCollectCardsView.refreshControl.rx.controlEvent(.valueChanged) + .withLatestFrom(isRefreshing) + .filter { $0 == false } + .delay(.milliseconds(1000), scheduler: MainScheduler.instance) + .map { _ in Reactor.Action.refresh } + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + + let isFavorite = reactor.state.map(\.isFavorite).share() + self.rightFavoriteButton.rx.throttleTap + .withLatestFrom(isFavorite) + .map(Reactor.Action.updateIsFavorite) + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + + // State + isRefreshing .observe(on: MainScheduler.asyncInstance) - .subscribe(with: self) { object, searchTerms in - object.searchTermsView.setModels(searchTerms) - object.searchTermsView.isHidden = false + .filter { $0 == false } + .subscribe(with: self.tagCollectCardsView) { tagCollectCardsView, _ in + tagCollectCardsView.isRefreshing = false } .disposed(by: self.disposeBag) Observable.combineLatest( - searchTerms.filterNil(), - self.searchTextFieldView.textFieldDidReturn, - resultSelector: { ($0, $1) } + searchTerms.distinctUntilChanged(), + tagCardInfos.distinctUntilChanged() ) .observe(on: MainScheduler.asyncInstance) - .subscribe(with: self) { object, searchTermInfos in - let (searchTerms, returnKeyDidTap) = searchTermInfos + .subscribe(with: self) { object, combined in + let (searchTerms, tagCardInfos) = combined - object.searchTermsView.setModels(searchTerms, with: returnKeyDidTap != nil) - object.searchTermsView.isHidden = false + if let searchTerms = searchTerms { object.searchTermsView.setModels(searchTerms) } + if let tagCardInfos = tagCardInfos { object.tagCollectCardsView.setModels(tagCardInfos) } + + object.searchTermsView.isHidden = tagCardInfos?.isEmpty ?? false + object.tagCollectCardsView.isHidden = tagCardInfos?.isEmpty ?? true } .disposed(by: self.disposeBag) + Observable.combineLatest(searchTerms, tagCardInfos) + .map { ($0 == nil, $1 == nil) } + .observe(on: MainScheduler.asyncInstance) + .subscribe(with: self) { object, combined in + let (isSearchTermsNil, isTagCardInfosNil) = combined + + object.searchTermsView.isHidden = isSearchTermsNil + object.tagCollectCardsView.isHidden = isTagCardInfosNil + } + .disposed(by: self.disposeBag) + + self.searchTextFieldView.textFieldDidReturn + .withLatestFrom(searchTerms.filterNil()) + .observe(on: MainScheduler.asyncInstance) + .subscribe(with: self) { object, models in + object.searchTermsView.setModels(models, with: true) + object.searchTermsView.isHidden = false + } + .disposed(by: self.disposeBag) + + isFavorite + .observe(on: MainScheduler.asyncInstance) + .subscribe(with: self) { object, isFavorite in + object.rightFavoriteButton.foregroundColor = isFavorite ? .som.v2.yMain : .som.v2.gray200 + } + .disposed(by: self.disposeBag) + + let isUpdated = reactor.state.map(\.isUpdated).distinctUntilChanged().filterNil() + isUpdated + .filter { $0 } + .withLatestFrom(isFavorite) + .observe(on: MainScheduler.instance) + .subscribe(onNext: { isFavorite in + if isFavorite { + GAHelper.shared.logEvent(event: GAEvent.TagView.favoriteTagRegister_btnClick) + } + + let message = isFavorite ? Text.addToastMessage : Text.deleteToastMessage + let bottomToastView = SOMBottomToastView( + title: "‘\(reactor.currentState.selectedTagInfo?.title ?? "")’" + message, + actions: nil + ) + + var wrapper: SwiftEntryKitViewWrapper = bottomToastView.sek + wrapper.entryName = Text.bottomToastEntryName + wrapper.showBottomToast(verticalOffset: 34 + 8) + }) + .disposed(by: self.disposeBag) + + isUpdated + .filter { $0 == false } + .withLatestFrom(isFavorite) + .observe(on: MainScheduler.instance) + .subscribe(onNext: { isFavorite in + let actions = [ + SOMBottomToastView.ToastAction(title: Text.failToastActionTitle, action: { + SwiftEntryKit.dismiss(.specific(entryName: Text.bottomToastEntryNameWithAction)) { + reactor.action.onNext(.updateIsFavorite(isFavorite)) + } + }) + ] + let bottomToastView = SOMBottomToastView(title: Text.failedToastMessage, actions: actions) + + var wrapper: SwiftEntryKitViewWrapper = bottomToastView.sek + wrapper.entryName = Text.bottomToastEntryNameWithAction + wrapper.showBottomToast(verticalOffset: 34 + 8) + }) + .disposed(by: self.disposeBag) + + reactor.state.map(\.hasErrors) + .distinctUntilChanged() + .filterNil() + .filter { $0 } + .observe(on: MainScheduler.instance) + .subscribe(onNext: { _ in + let bottomToastView = SOMBottomToastView(title: Text.addAdditionalLimitedFloatMessage, actions: nil) + + var wrapper: SwiftEntryKitViewWrapper = bottomToastView.sek + wrapper.entryName = Text.bottomToastEntryNameWithoutAction + wrapper.showBottomToast(verticalOffset: 34 + 8) + }) + .disposed(by: self.disposeBag) + + let cardIsDeleted = reactor.state.map(\.cardIsDeleted) + .distinctUntilChanged(reactor.canPushToDetail) + .filterNil() + cardIsDeleted + .filter { $0.isDeleted } + .map { $0.selectedId } + .observe(on: MainScheduler.instance) + .subscribe(with: self) { object, selectedId in + object.showPungedCardDialog(reactor, with: selectedId) + } + .disposed(by: self.disposeBag) + cardIsDeleted + .filter { $0.isDeleted == false } + .map { $0.selectedId } + .do(onNext: { _ in + reactor.action.onNext(.cleanup(.push)) + + GAHelper.shared.logEvent( + event: GAEvent.DetailView.cardDetail_tracePathClick( + previous_path: .tag_search_collect + ) + ) + }) + .observe(on: MainScheduler.instance) + .subscribe(with: self) { object, selectedId in + let detailViewController = DetailViewController() + detailViewController.reactor = reactor.reactorForDetail(with: selectedId) + object.navigationPush(detailViewController, animated: true) + } + .disposed(by: self.disposeBag) + } +} + + +// MARK: Show dialog + +private extension TagSearchViewController { + + func showPungedCardDialog(_ reactor: TagSearchViewReactor, with selectedId: String) { + + let confirmAction = SOMDialogAction( + title: Text.confirmActionTitle, + style: .primary, + action: { + SOMDialogViewController.dismiss { + reactor.action.onNext(.cleanup(.push)) + + let tagCardInfos = reactor.currentState.tagCardInfos ?? [] + reactor.action.onNext( + .updateTagCards( + tagCardInfos.filter { $0.id != selectedId } + ) + ) + } + } + ) + + SOMDialogViewController.show( + title: Text.pungedCardDialogTitle, + messageView: nil, + textAlignment: .left, + actions: [confirmAction] + ) } } diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/Search/TagSearchViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Tags/Search/TagSearchViewReactor.swift index e1fd8d30..f9f5a756 100644 --- a/SOOUM/SOOUM/Presentations/Main/Tags/Search/TagSearchViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Tags/Search/TagSearchViewReactor.swift @@ -10,42 +10,132 @@ import ReactorKit class TagSearchViewReactor: Reactor { enum Action: Equatable { - case reset + // 검색어 조회 case search(String) + // 태그 모아보기 + case cardsWithTag(TagInfoForCard) + case refresh + case more(String) + case updateTagCards([ProfileCardInfo]) + case hasDetailCard(String) + case updateIsFavorite(Bool) + case cleanup(CleanupFor) } enum Mutation { case searchTerms([TagInfo]?) - case updateIsUpdate(Bool?) + case selectedTagInfo(TagInfoForCard?) + case tagCardInfos([ProfileCardInfo]?) + case moreFind([ProfileCardInfo]) + case cardIsDeleted((String, Bool)?) + case updateIsUpdated(Bool?) + case updateIsFavorite(Bool) + case updateIsRefreshing(Bool) + case updateHasErrors(Bool?) } struct State { fileprivate(set) var searchTerms: [TagInfo]? + fileprivate(set) var selectedTagInfo: TagInfoForCard? + fileprivate(set) var tagCardInfos: [ProfileCardInfo]? + fileprivate(set) var cardIsDeleted: (selectedId: String, isDeleted: Bool)? fileprivate(set) var isUpdated: Bool? + fileprivate(set) var isFavorite: Bool + fileprivate(set) var isRefreshing: Bool + fileprivate(set) var hasErrors: Bool? } var initialState: State = .init( searchTerms: nil, - isUpdated: nil + selectedTagInfo: nil, + tagCardInfos: nil, + cardIsDeleted: nil, + isUpdated: nil, + isFavorite: false, + isRefreshing: false, + hasErrors: nil ) private let dependencies: AppDIContainerable private let fetchTagUseCase: FetchTagUseCase + private let fetchCardUseCase: FetchCardUseCase + private let fetchCardDetailUseCase: FetchCardDetailUseCase + private let updateTagFavoriteUseCase: UpdateTagFavoriteUseCase init(dependencies: AppDIContainerable) { self.dependencies = dependencies self.fetchTagUseCase = dependencies.rootContainer.resolve(FetchTagUseCase.self) + self.fetchCardUseCase = dependencies.rootContainer.resolve(FetchCardUseCase.self) + self.fetchCardDetailUseCase = dependencies.rootContainer.resolve(FetchCardDetailUseCase.self) + self.updateTagFavoriteUseCase = dependencies.rootContainer.resolve(UpdateTagFavoriteUseCase.self) } func mutate(action: Action) -> Observable { switch action { - case .reset: - - return .just(.searchTerms(nil)) case let .search(terms): return self.fetchTagUseCase.related(keyword: terms, size: 20) .map(Mutation.searchTerms) + case let .cardsWithTag(tagInfo): + + return .concat([ + .just(.selectedTagInfo(tagInfo)), + self.cardsWithTag(tagInfo, with: nil) + .catchAndReturn(.tagCardInfos([])) + ]) + case .refresh: + + guard let tagInfo = self.currentState.selectedTagInfo else { return .empty() } + + return .concat([ + .just(.updateIsRefreshing(true)), + self.cardsWithTag(tagInfo, with: nil) + .catchAndReturn(.tagCardInfos([])), + .just(.updateIsRefreshing(false)) + ]) + case let .more(lastId): + + guard let tagInfo = self.currentState.selectedTagInfo else { return .empty() } + + return self.fetchCardUseCase.cardsWithTag(tagId: tagInfo.id, lastId: lastId) + .map(\.cardInfos) + .map(Mutation.moreFind) + case let .updateTagCards(tagCardInfos): + + return .just(.tagCardInfos(tagCardInfos)) + case let .hasDetailCard(selectedId): + + return .concat([ + .just(.cardIsDeleted(nil)), + self.fetchCardDetailUseCase.isDeleted(cardId: selectedId) + .map { (selectedId, $0) } + .map(Mutation.cardIsDeleted) + ]) + case let .updateIsFavorite( isFavorite): + + guard let tagInfo = self.currentState.selectedTagInfo else { return .empty() } + + return .concat([ + .just(.updateIsUpdated(nil)), + .just(.updateHasErrors(nil)), + self.updateTagFavoriteUseCase.updateFavorite(tagId: tagInfo.id, isFavorite: !isFavorite) + .flatMapLatest { isUpdated -> Observable in + + let isFavorite = isUpdated ? !isFavorite : isFavorite + return .concat([ + .just(.updateIsFavorite(isFavorite)), + .just(.updateIsUpdated(isUpdated)) + ]) + } + .catch(self.catchClosure) + ]) + case let .cleanup(cleanupFor): + + switch cleanupFor { + case .push: return .just(.cardIsDeleted(nil)) + case .search: return .just(.searchTerms(nil)) + case .tagCard: return .just(.tagCardInfos(nil)) + } } } @@ -54,16 +144,94 @@ class TagSearchViewReactor: Reactor { switch mutation { case let .searchTerms(searchTerms): newState.searchTerms = searchTerms - case let .updateIsUpdate(isUpdated): + case let .selectedTagInfo(selectedTagInfo): + newState.selectedTagInfo = selectedTagInfo + case let .tagCardInfos(tagCardInfos): + newState.tagCardInfos = tagCardInfos + case let .moreFind(tagCardInfos): + newState.tagCardInfos? += tagCardInfos + case let .cardIsDeleted(cardIsDeleted): + newState.cardIsDeleted = cardIsDeleted + case let .updateIsUpdated(isUpdated): newState.isUpdated = isUpdated + case let .updateIsFavorite(isFavorite): + newState.isFavorite = isFavorite + case let .updateIsRefreshing(isRefreshing): + newState.isRefreshing = isRefreshing + case let .updateHasErrors(hasErrors): + newState.hasErrors = hasErrors } return newState } } +private extension TagSearchViewReactor { + + func cardsWithTag(_ tagInfo: TagInfoForCard, with lastId: String?) -> Observable { + + return self.fetchCardUseCase.cardsWithTag(tagId: tagInfo.id, lastId: nil) + .withUnretained(self) + .flatMapLatest { object, tagCardsInfo -> Observable in + + if tagCardsInfo.cardInfos.isEmpty { + let tagInfo = FavoriteTagInfo(id: tagInfo.id, title: tagInfo.title) + return object.fetchTagUseCase.isFavorites(with: tagInfo) + .flatMapLatest { isFavorite -> Observable in + + return .concat([ + .just(.tagCardInfos(tagCardsInfo.cardInfos)), + .just(.updateIsFavorite(isFavorite)) + ]) + } + } + + return .concat([ + .just(.tagCardInfos(tagCardsInfo.cardInfos)), + .just(.updateIsFavorite(tagCardsInfo.isFavorite)) + ]) + } + } + + var catchClosure: ((Error) throws -> Observable) { + return { error in + + let nsError = error as NSError + + if case 400 = nsError.code { + + return .just(.updateHasErrors(true)) + } + + return .just(.updateIsUpdated(false)) + } + } +} + extension TagSearchViewReactor { - func reactorForSearchCollect(with id: String, title: String) -> TagSearchCollectViewReactor { - TagSearchCollectViewReactor(dependencies: self.dependencies, with: id, title: title) + func canPushToDetail( + prev prevCardIsDeleted: (selectedId: String, isDeleted: Bool)?, + curr currCardIsDeleted: (selectedId: String, isDeleted: Bool)? + ) -> Bool { + return prevCardIsDeleted?.selectedId == currCardIsDeleted?.selectedId && + prevCardIsDeleted?.isDeleted == currCardIsDeleted?.isDeleted + } + + func reactorForDetail(with id: String) -> DetailViewReactor { + DetailViewReactor(dependencies: self.dependencies, with: id) + } +} + +extension TagSearchViewReactor { + + struct TagInfoForCard: Equatable { + let id: String + let title: String + } + + enum CleanupFor { + case push + case search + case tagCard } } diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/Search/Views/SearchTextFieldView+Rx.swift b/SOOUM/SOOUM/Presentations/Main/Tags/Search/Views/SearchTextFieldView+Rx.swift new file mode 100644 index 00000000..810d07cf --- /dev/null +++ b/SOOUM/SOOUM/Presentations/Main/Tags/Search/Views/SearchTextFieldView+Rx.swift @@ -0,0 +1,16 @@ +// +// SearchTextFieldView+Rx.swift +// SOOUM +// +// Created by 오현식 on 1/2/26. +// + +import RxCocoa +import RxSwift + +extension Reactive where Base: SearchTextFieldView { + + var text: ControlProperty { + self.base.textField.rx.text + } +} diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/Search/Views/SearchTextFieldView.swift b/SOOUM/SOOUM/Presentations/Main/Tags/Search/Views/SearchTextFieldView.swift index 673250e1..aae21c39 100644 --- a/SOOUM/SOOUM/Presentations/Main/Tags/Search/Views/SearchTextFieldView.swift +++ b/SOOUM/SOOUM/Presentations/Main/Tags/Search/Views/SearchTextFieldView.swift @@ -72,7 +72,6 @@ class SearchTextFieldView: UIView { var text: String? { set { self.textField.text = newValue - self.textField.sendActions(for: .editingChanged) } get { return self.textField.text @@ -103,7 +102,7 @@ class SearchTextFieldView: UIView { return self.text?.isEmpty ?? true } - let textFieldDidReturn = PublishRelay() + let textFieldDidReturn = PublishRelay() // MARK: Initalization @@ -213,7 +212,7 @@ extension SearchTextFieldView: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() - self.textFieldDidReturn.accept(textField.text) + self.textFieldDidReturn.accept(()) return true } } diff --git a/SOOUM/SOOUM/Presentations/Main/Write/Views/Tags/WriteCardTagFooter.swift b/SOOUM/SOOUM/Presentations/Main/Write/Views/Tags/WriteCardTagFooter.swift index 826334c3..8571310b 100644 --- a/SOOUM/SOOUM/Presentations/Main/Write/Views/Tags/WriteCardTagFooter.swift +++ b/SOOUM/SOOUM/Presentations/Main/Write/Views/Tags/WriteCardTagFooter.swift @@ -161,24 +161,12 @@ extension WriteCardTagFooter: UITextFieldDelegate { } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - // 타이핑 시 공백 제거 - if string == " " && range.length == 0 { - return false - } - // 붙여넣기 공백 제거 - let isPasting: Bool = string.count > 1 || range.length > 0 - var newString: String = string - if isPasting { - newString = string.replacingOccurrences(of: " ", with: "") - if newString.isEmpty && string.contains(" ") { - return false - } - } return textField.shouldChangeCharactersIn( in: range, - replacementString: newString, - maxCharacters: Constants.maxCharacters + replacementString: string, + maxCharacters: Constants.maxCharacters, + hasSpaces: false ) }