diff --git a/Podfile b/Podfile index 63c99b6..d0a506d 100644 --- a/Podfile +++ b/Podfile @@ -13,19 +13,16 @@ target 'TiquiTaca_iOS' do pod 'ExytePopupView', '= 1.1.4' # Firebase - pod 'Firebase/Analytics', '= 8.15.0' - pod 'Firebase/Messaging', '= 8.15.0' + pod 'FirebaseAnalytics', '= 9.1.0' + pod 'FirebaseCrashlytics', '= 9.1.0' + pod 'FirebaseMessaging', '= 9.1.0' + pod 'FirebasePerformance', '= 9.2.0' # DB pod 'RealmSwift', '= 10.25.1' - # Security - pod 'KeychainAccess', '= 4.2.2' - pod 'CryptoSwift', '= 1.4.3' - # Util pod 'SwiftLint', '= 0.47.0' - pod 'R.swift', '= 6.1.0' pod 'LicensePlist', '= 3.22.0' # Socket diff --git a/README.md b/README.md index 3c5a3ed..e9fc2ed 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,12 @@
실시간으로 궁금한 장소에 대해 정보를 공유하는 만남의 광장, 티키타카⚡️
+
+ + Download on the App Store + +
+ --- ![github_img2](https://user-images.githubusercontent.com/13329304/176240116-90cccb5f-e0b4-4437-94d2-523e6ac2150b.png) @@ -35,34 +41,41 @@ ![github_img7](https://user-images.githubusercontent.com/13329304/176240464-0d1abadc-5563-4358-8244-18bb7277491f.png) -
⚡️ 같이 티키타카할 준비가 됐다면 망설이지 말고 놀러오세요:)⚡️
- -
+
+ ⚡️ 같이 티키타카할 준비가 됐다면 망설이지 말고 놀러오세요:)⚡️ +

+
--- ## **🛠 Tech Stack** ### Project -- SwiftUI -- Combine -- TCA(The Composable Architecture) +|Based| +|:---| +|SwiftUI| +|Combine| +|TCA (The Composable Architecture)| + +![github_img2](https://user-images.githubusercontent.com/13329304/176240116-90cccb5f-e0b4-4437-94d2-523e6ac2150b.png) -
### Dependency |Core| |:---| |[ComposableArchitecture](https://github.com/pointfreeco/swift-composable-architecture)| |[ComposableCoreLocation](https://github.com/pointfreeco/composable-core-location)| - |[Firebase/Messaging](https://github.com/firebase/firebase-ios-sdk)| - |[Firebase/Analytics](https://github.com/firebase/firebase-ios-sdk)| + |[Firebase](https://github.com/firebase/firebase-ios-sdk)| |[Socket.IO-Client-Swift](https://github.com/socketio/socket.io-client-swift)| |Security| |:---| |[KeyChainAccess](https://github.com/kishikawakatsumi/KeychainAccess)| + |DB| + |:---| + |[RealmSwift](https://github.com/realm/realm-swift)| + |UI| |:---| |[Map](https://github.com/pauljohanneskraft/Map)| diff --git a/TiquiTaca_iOS.xcodeproj/project.pbxproj b/TiquiTaca_iOS.xcodeproj/project.pbxproj index 1447e31..4ff6a87 100644 --- a/TiquiTaca_iOS.xcodeproj/project.pbxproj +++ b/TiquiTaca_iOS.xcodeproj/project.pbxproj @@ -128,7 +128,6 @@ F2F1EDA828258E0E00AC4FDD /* AppService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2F1EDA728258E0E00AC4FDD /* AppService.swift */; }; F2F1EDAF2829335A00AC4FDD /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2F1EDAE2829335A00AC4FDD /* AppView.swift */; }; F2F1EDB12829336200AC4FDD /* AppCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2F1EDB02829336200AC4FDD /* AppCore.swift */; }; - F2FA365C2808897C00A24789 /* R.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2FA365B2808897C00A24789 /* R.generated.swift */; }; F2FDA216280BBDAC00B2ED75 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F2FDA215280BBDAC00B2ED75 /* LaunchScreen.storyboard */; }; F2FDA21A280BBF8300B2ED75 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2FDA219280BBF8300B2ED75 /* AppDelegate.swift */; }; F2FDA228280C0EE300B2ED75 /* TermsOfServiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2FDA227280C0EE300B2ED75 /* TermsOfServiceView.swift */; }; @@ -341,7 +340,6 @@ F2F1EDAE2829335A00AC4FDD /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = ""; }; F2F1EDB02829336200AC4FDD /* AppCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCore.swift; sourceTree = ""; }; F2F1FFBE28049D7E00F3A8E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = TiquiTaca_iOS/SupportingFile/Info.plist; sourceTree = SOURCE_ROOT; }; - F2FA365B2808897C00A24789 /* R.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = R.generated.swift; sourceTree = SOURCE_ROOT; }; F2FDA215280BBDAC00B2ED75 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; F2FDA219280BBF8300B2ED75 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F2FDA227280C0EE300B2ED75 /* TermsOfServiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServiceView.swift; sourceTree = ""; }; @@ -550,7 +548,6 @@ F2F1FFBE28049D7E00F3A8E4 /* Info.plist */, F2F1ED6E2817ABE900AC4FDD /* GoogleService-Info.plist */, F24B1EEE28086CA0003D4E1D /* .swiftlint.yml */, - F2FA365B2808897C00A24789 /* R.generated.swift */, ); path = SupportingFile; sourceTree = ""; @@ -1034,7 +1031,6 @@ buildPhases = ( BD3CF7E3E7E022E0969F7A18 /* [CP] Check Pods Manifest.lock */, F2F1FFBF2804A2AA00F3A8E4 /* Run Swiftlint */, - F24B1EF12808728F003D4E1D /* R.swift */, F2C02C6527F8824A004C4235 /* Sources */, F2C02C6627F8824A004C4235 /* Frameworks */, F2C02C6727F8824A004C4235 /* Resources */, @@ -1196,26 +1192,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-TiquiTaca_iOS/Pods-TiquiTaca_iOS-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - F24B1EF12808728F003D4E1D /* R.swift */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = R.swift; - outputFileListPaths = ( - ); - outputPaths = ( - $SRCROOT/R.generated.swift, - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$PODS_ROOT/R.swift/rswift\" generate \"$SRCROOT/R.generated.swift\"\n"; - }; F2F1FFBF2804A2AA00F3A8E4 /* Run Swiftlint */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -1335,7 +1311,6 @@ F96CDE772824EF9B002DEB90 /* MyInfoCore.swift in Sources */, F25C79B8282CB5F2009C60EE /* CheckNicknameEntity.swift in Sources */, F21276462811C6D500FB568D /* TOSFieldListViewCore.swift in Sources */, - F2FA365C2808897C00A24789 /* R.generated.swift in Sources */, F22184FD284C9EED005CE6C3 /* ChatRoomListSortType.swift in Sources */, 8601F65C281309B10085F692 /* SplashView.swift in Sources */, F25C79BA282D7524009C60EE /* String+Extensions.swift in Sources */, @@ -1548,7 +1523,7 @@ CODE_SIGN_ENTITLEMENTS = TiquiTaca_iOS/TiquiTaca_iOSDebug.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 19; + CURRENT_PROJECT_VERSION = 24; DEVELOPMENT_ASSET_PATHS = "\"TiquiTaca_iOS/Resource/Preview Content\""; DEVELOPMENT_TEAM = 75FNRKTJ9S; ENABLE_PREVIEWS = YES; @@ -1564,7 +1539,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = "com.tiquitaca.TiquiTaca-iOS"; PRODUCT_NAME = TiquiTaca; PROVISIONING_PROFILE_SPECIFIER = dev_tiquiTaca; @@ -1583,7 +1558,7 @@ CODE_SIGN_ENTITLEMENTS = TiquiTaca_iOS/TiquiTaca_iOS.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution: Minseok Kang (75FNRKTJ9S)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 19; + CURRENT_PROJECT_VERSION = 24; DEVELOPMENT_ASSET_PATHS = "\"TiquiTaca_iOS/Resource/Preview Content\""; DEVELOPMENT_TEAM = 75FNRKTJ9S; ENABLE_PREVIEWS = YES; @@ -1599,7 +1574,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = "com.tiquitaca.TiquiTaca-iOS"; PRODUCT_NAME = TiquiTaca; PROVISIONING_PROFILE_SPECIFIER = appStore_tiquiTaca; diff --git a/TiquiTaca_iOS/AppDelegate.swift b/TiquiTaca_iOS/AppDelegate.swift index 00d4db7..7c9bda4 100644 --- a/TiquiTaca_iOS/AppDelegate.swift +++ b/TiquiTaca_iOS/AppDelegate.swift @@ -7,8 +7,12 @@ // import UIKit -import Firebase import UserNotifications +import FirebaseCore +import FirebaseAnalytics +import FirebaseMessaging +import FirebasePerformance +import FirebaseCrashlytics @main final class AppDelegate: NSObject, UIApplicationDelegate { @@ -16,9 +20,8 @@ final class AppDelegate: NSObject, UIApplicationDelegate { _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { - UIApplication.shared.applicationIconBadgeNumber = 0 - FirebaseApp.configure() + UIApplication.shared.applicationIconBadgeNumber = 0 Messaging.messaging().delegate = self UNUserNotificationCenter.current().delegate = self @@ -65,7 +68,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { completionHandler([.sound, .banner, .list]) } - + func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let userInfo = response.notification.request.content.userInfo Messaging.messaging().appDidReceiveMessage(userInfo) diff --git a/TiquiTaca_iOS/SupportingFile/Info.plist b/TiquiTaca_iOS/SupportingFile/Info.plist index 7ab5da3..22f99e8 100644 --- a/TiquiTaca_iOS/SupportingFile/Info.plist +++ b/TiquiTaca_iOS/SupportingFile/Info.plist @@ -47,11 +47,11 @@ NSLocationAlwaysAndWhenInUseUsageDescription - 회원님의 위치정보가 지도에 표시됩니다. 위치 정보는 근처 채팅방 정보를 불러올때 그리고 채팅시 장소 반경 내부에 있는지 표기할 때 활용됩니다. 다른사람들에게 해당 반경 내부에 있는지에 대한 정보가 노출 될 수 있습니다. 언제든지 해당 위치 정보 공유를 중지할 수 있습니다. + 위치 서비스를 켜면 내 주변 채팅가능 장소를 볼 수 있습니다. NSLocationAlwaysUsageDescription - 회원님의 위치정보가 지도에 표시됩니다. 위치 정보는 근처 채팅방 정보를 불러올때 그리고 채팅시 장소 반경 내부에 있는지 표기할 때 활용됩니다. 다른사람들에게 해당 반경 내부에 있는지에 대한 정보가 노출 될 수 있습니다. 언제든지 해당 위치 정보 공유를 중지할 수 있습니다. + 위치 서비스를 켜면 내 주변 채팅가능 장소를 볼 수 있습니다. NSLocationWhenInUseUsageDescription - 회원님의 위치정보가 지도에 표시됩니다. 위치 정보는 근처 채팅방 정보를 불러올때 그리고 채팅시 장소 반경 내부에 있는지 표기할 때 활용됩니다. 다른사람들에게 해당 반경 내부에 있는지에 대한 정보가 노출 될 수 있습니다. 언제든지 해당 위치 정보 공유를 중지할 수 있습니다. + 위치 서비스를 켜면 내 주변 채팅가능 장소를 볼 수 있습니다. UIAppFonts Pretendard-Bold.otf diff --git a/TiquiTaca_iOS/Views/Chat/ChatCore.swift b/TiquiTaca_iOS/Views/Chat/ChatCore.swift index 7b92b3e..e64ce2d 100644 --- a/TiquiTaca_iOS/Views/Chat/ChatCore.swift +++ b/TiquiTaca_iOS/Views/Chat/ChatCore.swift @@ -14,19 +14,21 @@ import SwiftUI struct ChatState: Equatable { enum Route { case chatDetail + case none } - var route: Route? - var isFirstLoad = true - var currentTab: RoomListType = .like + var route: Route? - var unReadChatCount: Int = 0 + var enteredRoom: RoomInfoEntity.Response? var lastChatLog: ChatLogEntity.Response? - var lastLoadTime: String = Date.current(type: .HHmm) - var enteredRoom: RoomInfoEntity.Response? + var unReadChatCount: Int = 0 + + var currentTab: RoomListType = .like var likeRoomList: [RoomInfoEntity.Response] = [] var popularRoomList: [RoomInfoEntity.Response] = [] + + var showRoomEnterPopup: Bool = false var moveToChatDetail: Bool = false var willEnterRoom: RoomInfoEntity.Response? @@ -34,30 +36,31 @@ struct ChatState: Equatable { } enum ChatAction: Equatable { - case onAppear + // MARK: Request Action case fetchEnteredRoomInfo case fetchLikeRoomList case fetchPopularRoomList - + // MARK: Socket Action case socketConnected(String) case socketDisconnected(String) case socketResponse(SocketBannerService.Action) - + // MARK: Response Action case responsePopularRoomList(Result<[RoomInfoEntity.Response]?, HTTPError>) case responseLikeRoomList(Result<[RoomInfoEntity.Response]?, HTTPError>) case responseEnteredRoom(Result) case responseRoomFavorite(Result) - + // MARK: User Action case tabChange(RoomListType) case removeFavoriteRoom(RoomInfoEntity.Response) case willEnterRoom(RoomInfoEntity.Response) case refresh - - - case chatDetailAction(ChatDetailAction) + // MARK: Router Action case setRoute(ChatState.Route?) + case setShowRoomEnterPopup(Bool) case setMoveToChatDetail(Bool) + // MARK: Child Action + case chatDetailAction(ChatDetailAction) } struct ChatEnvironment { @@ -96,10 +99,7 @@ let chatCore = Reducer< > { state, action, environment in switch action { case .onAppear: - guard state.isFirstLoad else { return .none } - state.lastLoadTime = Date.current(type: .HHmm) - state.isFirstLoad = true return .merge( Effect(value: .fetchEnteredRoomInfo) .eraseToEffect(), @@ -108,7 +108,7 @@ let chatCore = Reducer< Effect(value: .fetchPopularRoomList) .eraseToEffect() ) - // MARK: Requeset + // MARK: Requeset case .fetchEnteredRoomInfo: return environment.appService.roomService .getEnteredRoom() @@ -133,7 +133,7 @@ let chatCore = Reducer< .receive(on: environment.mainQueue) .catchToEffect() .map(ChatAction.responseRoomFavorite) - // MARK: Response + // MARK: Response case let .responseRoomFavorite(.success(res)): return Effect(value: .fetchLikeRoomList) .eraseToEffect() @@ -188,7 +188,7 @@ let chatCore = Reducer< .responsePopularRoomList(.failure), .responseRoomFavorite(.failure): return .none - // MARK: View Action + // MARK: View Action case .tabChange(let type): guard state.currentTab != type else { return .none } state.currentTab = type @@ -200,10 +200,16 @@ let chatCore = Reducer< return .none case let .setRoute(route): state.route = route + if route == .chatDetail { + state.moveToChatDetail = true + } + return .none + case let .setShowRoomEnterPopup(isPresented): + state.showRoomEnterPopup = isPresented return .none case let .setMoveToChatDetail(isMoveToChatDetail): + state.route = isMoveToChatDetail ? .chatDetail : ChatState.Route.none state.moveToChatDetail = isMoveToChatDetail - state.route = isMoveToChatDetail ? .chatDetail : nil return .none case .refresh: state.lastLoadTime = Date.current(type: .HHmm) @@ -215,6 +221,5 @@ let chatCore = Reducer< ) case .chatDetailAction: return .none - } } diff --git a/TiquiTaca_iOS/Views/Chat/ChatView.swift b/TiquiTaca_iOS/Views/Chat/ChatView.swift index cd52abe..5283c7a 100644 --- a/TiquiTaca_iOS/Views/Chat/ChatView.swift +++ b/TiquiTaca_iOS/Views/Chat/ChatView.swift @@ -11,94 +11,97 @@ import ComposableArchitecture import TTDesignSystemModule struct ChatView: View { - @State private var showPopup: Bool = false var store: Store - @StateObject private var viewStore: ViewStore + @ObservedObject private var viewStore: ViewStore struct ViewState: Equatable { - let currentTab: RoomListType - let lastLoadTime: String + let enteredRoom: RoomInfoEntity.Response? + let lastChatLog: ChatLogEntity.Response? let unReadChatCount: Int - let enteredRoom: RoomInfoEntity.Response? + let currentTab: RoomListType + let lastLoadTime: String let likeRoomList: [RoomInfoEntity.Response] let popularRoomList: [RoomInfoEntity.Response] + + let showRoomEnterPopup: Bool let moveToChatDetail: Bool + let route: ChatState.Route? init(state: ChatState) { - currentTab = state.currentTab - lastLoadTime = state.lastLoadTime + enteredRoom = state.enteredRoom + lastChatLog = state.lastChatLog unReadChatCount = state.unReadChatCount - enteredRoom = state.enteredRoom + currentTab = state.currentTab + lastLoadTime = state.lastLoadTime likeRoomList = state.likeRoomList popularRoomList = state.popularRoomList + + showRoomEnterPopup = state.showRoomEnterPopup moveToChatDetail = state.moveToChatDetail + route = state.route } } init(store: Store) { self.store = store - self._viewStore = StateObject(wrappedValue: ViewStore(store.scope(state: ViewState.init))) + self.viewStore = ViewStore(store.scope(state: ViewState.init)) } var body: some View { VStack(spacing: 0) { VStack(spacing: .spacingM) { - Text("채팅방") - .font(.heading1) - .foregroundColor(.white) - .padding(.horizontal, .spacingXL) - .padding(.top, .spacingXL) - .hLeading() - - EnteredRoomView( - store: store, - moveToChatDetail: viewStore.binding( - get: \.moveToChatDetail, - send: ChatAction.setMoveToChatDetail - ) - ) - - TabKindView( - currentTab: viewStore.binding( - get: \.currentTab, - send: ChatAction.tabChange - ), - currentTime: viewStore.lastLoadTime - ) + title + enteredRoomView + tabKindView } - .background(Color.black800) + .background(Color.black800) - RoomListView( - store: store, - type: viewStore.currentTab, - showPopup: $showPopup, - moveToChatDetail: viewStore.binding( - get: \.moveToChatDetail, - send: ChatAction.setMoveToChatDetail - ) - ) - .background(.white) + RoomListView(store: store) + .background(.white) NavigationLink( - destination: ChatDetailView( - store: store.scope( - state: \.chatDetailState, - action: ChatAction.chatDetailAction), - shouldPopToRootView: viewStore.binding( - get: \.moveToChatDetail, - send: ChatAction.setMoveToChatDetail - ) + tag: ChatState.Route.chatDetail, + selection: viewStore.binding( + get: \.route, + send: ChatAction.setRoute ), - isActive: viewStore.binding( - get: \.moveToChatDetail, - send: ChatAction.setMoveToChatDetail - ) - ) { EmptyView() } + destination: { + ChatDetailView( + store: store.scope( + state: \.chatDetailState, + action: ChatAction.chatDetailAction + ), + shouldPopToRootView: viewStore.binding ( + get: \.moveToChatDetail, + send: ChatAction.setMoveToChatDetail + ) + ) + }, + label: EmptyView.init + ) .isDetailLink(false) .frame(height: 0) .hidden() +// NavigationLink( +// destination: ChatDetailView( +// store: store.scope( +// state: \.chatDetailState, +// action: ChatAction.chatDetailAction), +// shouldPopToRootView: viewStore.binding( +// get: \.moveToChatDetail, +// send: ChatAction.setMoveToChatDetail +// ) +// ), +// isActive: viewStore.binding( +// get: \.moveToChatDetail, +// send: ChatAction.setMoveToChatDetail +// ) +// ) { EmptyView() } +// .isDetailLink(false) +// .frame(height: 0) +// .hidden() } .listStyle(.plain) .navigationTitle("채팅방") @@ -108,34 +111,21 @@ struct ChatView: View { } } -// MARK: Current Chat View -private struct EnteredRoomView: View { - typealias State = ChatState - typealias Action = ChatAction - - private let store: Store - @Binding private var moveToChatDetail: Bool - @ObservedObject private var viewStore: ViewStore - - struct ViewState: Equatable { - let enteredRoom: RoomInfoEntity.Response? - let lastChatLog: ChatLogEntity.Response? - let unReadChatCount: Int - - init(state: State) { - enteredRoom = state.enteredRoom - lastChatLog = state.lastChatLog - unReadChatCount = state.unReadChatCount - } +// MARK: Title View +extension ChatView { + var title: some View { + Text("채팅방") + .font(.heading1) + .foregroundColor(.white) + .padding(.horizontal, .spacingXL) + .padding(.top, .spacingXL) + .hLeading() } - - init(store: Store, moveToChatDetail: Binding) { - self.store = store - viewStore = ViewStore(store.scope(state: ViewState.init)) - self._moveToChatDetail = moveToChatDetail - } - - var body: some View { +} + +// MARK: Entered Chat View +extension ChatView { + var enteredRoomView: some View { VStack { VStack { if viewStore.enteredRoom == nil { @@ -182,30 +172,30 @@ private struct EnteredRoomView: View { HStack { Text( viewStore.lastChatLog == nil ? - "아직 아무도 채팅을 치지 않았어요" : + "아직 아무도 채팅을 치지 않았어요" : (viewStore.lastChatLog?.getMessage() ?? "") ) - .foregroundColor(.white800) - .font(.body7) - .lineLimit(1) - .hLeading() + .foregroundColor(.white800) + .font(.body7) + .lineLimit(1) + .hLeading() VStack { Text( viewStore.unReadChatCount >= 100 ? - "+99" : + "+99" : "\(viewStore.unReadChatCount)" ) - .foregroundColor(viewStore.unReadChatCount == 0 ? Color.black600 : .black800) - .font(.body4) - .padding([.leading, .trailing], 6) - .padding([.top, .bottom], 2) + .foregroundColor(viewStore.unReadChatCount == 0 ? Color.black600 : .black800) + .font(.body4) + .padding([.leading, .trailing], 6) + .padding([.top, .bottom], 2) } .background(viewStore.unReadChatCount == 0 ? Color.black600 : Color.green900) .cornerRadius(11) } - .hLeading() + .hLeading() } - .padding(EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)) + .padding(EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)) } } .background(Color.black600) @@ -215,52 +205,30 @@ private struct EnteredRoomView: View { .onTapGesture { guard let room = viewStore.enteredRoom else { return } viewStore.send(.willEnterRoom(room)) - moveToChatDetail = true + viewStore.send(.setRoute(.chatDetail)) } } } } - // MARK: Tab Kind View -private struct TabKindView: View { - @Binding var currentTab: RoomListType - let currentTime: String - - private struct TabButton: ButtonStyle { - @Environment(\.isEnabled) var isEnabled - - public init() { } - public func makeBody(configuration: Configuration) -> some View { - return configuration.label - .frame(height: 40) - .font(.subtitle3) - .foregroundColor(isEnabled ? .black100 : .green500) - .padding([.leading, .trailing], 14) - .overlay( - Rectangle() - .frame(height: isEnabled ? 0 : 2) - .foregroundColor(Color.green500), - alignment: .bottom) - } - } - - var body: some View { +extension ChatView { + var tabKindView: some View { HStack(spacing: 0) { Spacer().frame(width: 10) Button( - action: { currentTab = .like }, + action: { viewStore.send(.tabChange(.like)) }, label: { Text("즐겨찾기") } ) .buttonStyle(TabButton()) - .disabled(currentTab == .like) + .disabled(viewStore.currentTab == .like) Button( - action: { currentTab = .popular }, + action: { viewStore.send(.tabChange(.popular)) }, label: { Text("인기채팅방") } ) .buttonStyle(TabButton()) - .disabled(currentTab == .popular) - Text(currentTime + " 기준") + .disabled(viewStore.currentTab == .popular) + Text(viewStore.lastLoadTime + " 기준") .foregroundColor(.white800) .font(.system(size: 13, weight: .semibold, design: .default)) .padding(.trailing, 24) @@ -270,60 +238,77 @@ private struct TabKindView: View { } } +private struct TabButton: ButtonStyle { + @Environment(\.isEnabled) var isEnabled + public init() { } + public func makeBody(configuration: Configuration) -> some View { + return configuration.label + .frame(height: 40) + .font(.subtitle3) + .foregroundColor(isEnabled ? .black100 : .green500) + .padding([.leading, .trailing], 14) + .overlay( + Rectangle() + .frame(height: isEnabled ? 0 : 2) + .foregroundColor(Color.green500), + alignment: .bottom + ) + } +} + +// MARK: RoomListView private struct RoomListView: View { - typealias Action = ChatAction + typealias CAction = ChatAction + typealias CState = ChatState - private let store: Store - let roomType: RoomListType - @Binding private var showPopup: Bool - @Binding private var moveToChatDetail: Bool - @ObservedObject private var viewStore: ViewStore - @State var selectedRoomUserCount: Int = 0 + private let maxUserCount = 300 + private let store: Store + @ObservedObject private var viewStore: ViewStore struct ViewState: Equatable { + let roomType: RoomListType let likeRoomList: [RoomInfoEntity.Response] let popularRoomList: [RoomInfoEntity.Response] let enteredRoom: RoomInfoEntity.Response? - init(state: ChatState) { + let willEnterRoom: RoomInfoEntity.Response? + let showRoomEnterPopup: Bool + + init(state: CState) { + roomType = state.currentTab likeRoomList = state.likeRoomList popularRoomList = state.popularRoomList enteredRoom = state.enteredRoom + + willEnterRoom = state.willEnterRoom + showRoomEnterPopup = state.showRoomEnterPopup } } - init( - store: Store, - type: RoomListType, - showPopup: Binding, - moveToChatDetail: Binding - ) { + init(store: Store) { self.store = store self.viewStore = ViewStore(store.scope(state: ViewState.init)) - self.roomType = type - self._showPopup = showPopup - self._moveToChatDetail = moveToChatDetail } var body: some View { List { - if (roomType == .like && viewStore.likeRoomList.isEmpty) || - (roomType == .popular && viewStore.popularRoomList.isEmpty) { - NoDataView(noDataType: roomType) + if (viewStore.roomType == .like && viewStore.likeRoomList.isEmpty) || + (viewStore.roomType == .popular && viewStore.popularRoomList.isEmpty) { + NoDataView(noDataType: viewStore.roomType) .padding(.top, .spacingXXXL * 2) .background(.white) } else { ForEach( - (roomType == .like ? + (viewStore.roomType == .like ? viewStore.likeRoomList : viewStore.popularRoomList ).enumerated().map({ $0 }), id: \.element.id ) { index, room in - RoomListCell(ranking: index + 1, info: room, type: roomType) + RoomListCell(ranking: index + 1, info: room, type: viewStore.roomType) .background(.white) .swipeActions(edge: .trailing, allowsFullSwipe: false, content: { - if roomType == .like { + if viewStore.roomType == .like { Button( action: { ViewStore(store).send(.removeFavoriteRoom(room)) @@ -335,12 +320,11 @@ private struct RoomListView: View { .onTapGesture(perform: { if viewStore.enteredRoom == nil || room.id == viewStore.enteredRoom?.id { viewStore.send(.willEnterRoom(room)) - moveToChatDetail = true + viewStore.send(.setRoute(.chatDetail)) } else { - selectedRoomUserCount = room.userCount ?? 0 UIView.setAnimationsEnabled(false) viewStore.send(.willEnterRoom(room)) - showPopup = true + viewStore.send(.setShowRoomEnterPopup(true)) } }) .listRowSeparator(.hidden) @@ -348,13 +332,19 @@ private struct RoomListView: View { } } } - .fullScreenCover(isPresented: $showPopup) { - AlertView( - isPopupPresent: $showPopup, - moveToChatDetailState: $moveToChatDetail, - roomUserCount: selectedRoomUserCount + .fullScreenCover( + isPresented: viewStore.binding( + get: \.showRoomEnterPopup, + send: ChatAction.setShowRoomEnterPopup ) - .background(BackgroundTransparentView()) + ) { + if viewStore.willEnterRoom?.userCount ?? 0 >= maxUserCount { + AlertView(store: store) + .overCapacity + } else { + AlertView(store: store) + .existEnteredRoom + } } .refreshable { viewStore.send(.refresh) @@ -364,21 +354,28 @@ private struct RoomListView: View { // MARK: Alert private struct AlertView: View { - @Binding var isPopupPresent: Bool - @Binding var moveToChatDetailState: Bool - let roomUserCount: Int - var maxUserCount = 300 - // 채팅방 인원 풀 -> 경고만 - // 채팅방 교체할 것인지 -> 경고 후 참가 - var body: some View { - if roomUserCount < maxUserCount { - existEnteredRoom - .padding(EdgeInsets(top: 0, leading: 24, bottom: 0, trailing: 24)) - } else { - overCapacity + private let store: Store + @ObservedObject var viewStore: ViewStore + + struct ViewState: Equatable { + let showRoomEnterPopup: Bool + let route: ChatState.Route? + + init(state: ChatState) { + showRoomEnterPopup = state.showRoomEnterPopup + route = state.route } } + init(store: Store) { + self.store = store + self.viewStore = ViewStore(store.scope(state: ViewState.init)) + } + + var body: some View { + existEnteredRoom + } + var existEnteredRoom: some View { TTPopupView.init( popUpCase: .oneLineTwoButton, @@ -387,19 +384,21 @@ private struct AlertView: View { leftButtonName: "취소", rightButtonName: "참여하기", confirm: { - isPopupPresent = false + viewStore.send(.setShowRoomEnterPopup(false)) DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - UIView.setAnimationsEnabled(true) - moveToChatDetailState = true + UIView.setAnimationsEnabled(true) + viewStore.send(.setRoute(.chatDetail)) } }, cancel: { - isPopupPresent = false + viewStore.send(.setShowRoomEnterPopup(false)) DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { UIView.setAnimationsEnabled(true) } } ) + .padding(EdgeInsets(top: 0, leading: 24, bottom: 0, trailing: 24)) + .background(BackgroundTransparentView()) } var overCapacity: some View { @@ -408,19 +407,15 @@ private struct AlertView: View { title: "해당 채팅방은 인원이 가득 찼어요", subtitle: "최대 인원수 300명이 차서 입장이 불가능해요", leftButtonName: "닫기", - confirm: { - isPopupPresent = false - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - UIView.setAnimationsEnabled(true) - } - }, cancel: { - isPopupPresent = false + viewStore.send(.setShowRoomEnterPopup(false)) DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { UIView.setAnimationsEnabled(true) } } ) + .padding(EdgeInsets(top: 0, leading: 24, bottom: 0, trailing: 24)) + .background(BackgroundTransparentView()) } } diff --git a/TiquiTaca_iOS/Views/MainMap/MainMapCore.swift b/TiquiTaca_iOS/Views/MainMap/MainMapCore.swift index 71b5791..1ba1e86 100644 --- a/TiquiTaca_iOS/Views/MainMap/MainMapCore.swift +++ b/TiquiTaca_iOS/Views/MainMap/MainMapCore.swift @@ -142,17 +142,17 @@ private let mainMapCore = Reducer< case .onLoad: state.isFirstLoad = true - - if environment.locationManager.authorizationStatus() == .notDetermined { - state.showLocationPopup = true - return .none - } +// if environment.locationManager.authorizationStatus() == .notDetermined { +// state.showLocationPopup = true +// return .none +// } return .init(value: .currentLocationButtonTapped) case .currentLocationButtonTapped: // 첫 위치 권한 설정, onLoad, 현위치 버튼 - if environment.locationManager.authorizationStatus() != .notDetermined && !environment.locationManager.locationServicesEnabled() { - return .init(value: .showLocationAlert) - } + guard environment.locationManager.locationServicesEnabled() else { return .init(value: .showLocationAlert) } +// if environment.locationManager.authorizationStatus() != .notDetermined && !environment.locationManager.locationServicesEnabled() { +// return .init(value: .showLocationAlert) +// } switch environment.locationManager.authorizationStatus() { case .restricted, .denied: diff --git a/TiquiTaca_iOS/Views/MyPage/MyPageView.swift b/TiquiTaca_iOS/Views/MyPage/MyPageView.swift index 686c5ff..dbbff14 100644 --- a/TiquiTaca_iOS/Views/MyPage/MyPageView.swift +++ b/TiquiTaca_iOS/Views/MyPage/MyPageView.swift @@ -199,7 +199,7 @@ private struct AlertView: View { .foregroundColor(.white700) .frame(height: 34) - Image("LinerRectangle") + Image("LinearRectangle") .overlay { VStack { Text("내가 받은 번개 갯수") diff --git a/TiquiTaca_iOS/Views/OtherProfile/OtherProfileView.swift b/TiquiTaca_iOS/Views/OtherProfile/OtherProfileView.swift index 3429df8..96cf208 100644 --- a/TiquiTaca_iOS/Views/OtherProfile/OtherProfileView.swift +++ b/TiquiTaca_iOS/Views/OtherProfile/OtherProfileView.swift @@ -204,8 +204,8 @@ struct OtherProfileView: View { } HStack(alignment: .center, spacing: 4) { - if viewStore.userInfo?.canUseFeature == true { - Image("bxLoudspeaker3") + if viewStore.userInfo?.canUseFeature == true && viewStore.userInfo?.level != 0 { + Image("ratingLv\(viewStore.userInfo?.level ?? 1)") .resizable() .frame(width: 32, height: 32) }