본문 바로가기
iOS

[iOS/SwiftUI] SwiftUI 에서 SocketiO 사용하기 (2)

by GODOLs 2023. 10. 11.

목차

    지난번 ViewModel + SocketiO 설정에 이어 SwiftUI에서 어떻게 구현되는지 설명 드리겠습니다.

    import SwiftUI
    import SDWebImageSwiftUI
    
    struct ChatView: View {
        @State private var messageText = ""
        @State private var messageTextHolder = "내용을 입력하세요"
        @State private var showPaymentAlert = false
        @State private var paymentSucceed = false
        @ObservedObject private var keyboardResponder = KeyboardResponder()
        @EnvironmentObject var chatViewModel:ChatViewModel
        @EnvironmentObject var appState:NavigationPathViewModel
        @Binding var roomId:String
        @Binding var partnerProfileImage:String
        @Binding var partnerNickname:String
        @Binding var partnerId:String
        @Binding var onAppearFromLobbyChat:Bool
        var firstChat:Bool
        @Environment(\.scenePhase) var scenePhase
        @State private var tabBarVisible: Bool = false
        @Environment(\.presentationMode) var presentationMode
        var buttonBack : some View {
            Button(action: {
                self.chatViewModel.leaveRoom(roomId: roomId)
                self.tabBarVisible.toggle()
                if self.firstChat {
                    self.appState.rootViewId = UUID()
                    self.appState.selectedTab = .tab3
    
                }else{
                    self.onAppearFromLobbyChat = true
                    self.presentationMode.wrappedValue.dismiss()
                }
    
            }) {
                HStack {
                    Image("back_button") // set image here
                        .aspectRatio(contentMode: .fit)
                        .foregroundColor(.white)
    
                }
            }
        }
    
        var body: some View {
            ZStack{
                VStack{
                    ScrollView{
                        ScrollViewReader { scrollView in
                            ChatIntroduceView()
                            ForEach(chatViewModel.messages.indices, id: \.self) { index in
                                ChatMessageRow(message: chatViewModel.messages[index], profileImage: partnerProfileImage, lobbyistId: partnerId)
                                    .id(index)
                                    .padding(.bottom,5)
                            }
                            .onChange(of: chatViewModel.messages.count) { _ in
                                // 새로운 메시지가 추가될 때마다 스크롤뷰를 최하단으로 이동시킵니다.
                                withAnimation {
                                    scrollView.scrollTo(chatViewModel.messages.count - 1, anchor: .bottom)
                                }
                            }
    
    
                        }
    
                    }
                    .hideKeyboardOnTap()
                    Spacer()
                    HStack{
                        RoundedRectangle(cornerRadius: 5)
                            .stroke(Color("brownish_grey"), lineWidth: 1)
                            .overlay(
    
                                    ZStack{
    
    
                                        TextEditor(text: $messageText)
                                            .modifier(Medium(size: 12))
    
                                            .frame(height:30)
                                            .foregroundColor(.black)
                                            .padding(15)
    
                                        if messageText.isEmpty{
                                            TextEditor(text: $messageTextHolder)
                                                .modifier(Medium(size: 12))
                                                .frame(height:30)
                                                .foregroundColor(Color("vivid_grey"))
                                                .padding(15)
                                                .opacity(messageText.isEmpty ? 1 : 0.5)
                                                .disabled(true)
    
                                        }
                                    }
    
    
    
                            )
                        Button(action: {
                            chatViewModel.sendMessage(messageText: messageText.trimmingCharacters(in: .whitespaces), roomId: roomId)
                            messageText = ""
                        }, label: {
                            Image(messageText.isEmpty ? "send" : "send_black")
                                .padding(.trailing,8)
                                .disabled(messageText.isEmpty)
                                .foregroundColor(Color("brownish_grey"))
                        })
                    }
                    .frame(height: 35)
                    .padding(.vertical,10)
                }
                .hideKeyboardOnTap()
                .navigationBarTitleDisplayMode(.inline)
                .toolbar {
                    ToolbarItem(placement: .navigationBarLeading) {
                        HStack {
                            buttonBack
    
                            WebImage(url: URL(string: "\(SHAREVAR.IMAGE_URL)/\(partnerProfileImage)")) // SDWebImageSwiftUI의 WebImage를 사용하여 프로필 이미지를 로드합니다.
                                .resizable()
                                .aspectRatio(contentMode: .fill)
                                .frame(width: 24, height: 24)
    
                                .clipShape(Circle())
                            Text(partnerNickname) // 상대방의 닉네임을 표시합니다.
                                .modifier(SemiBold(size: 15))
                        }
    
                    }
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button(action: {
                            withAnimation{
                                showPaymentAlert = true
                            }
    
                        }) {
                            RoundedRectangle(cornerRadius: 7)
                                .foregroundColor(.black)
                                .overlay(
                                    HStack{
                                        Image("payment_white")
                                            .resizable()
                                            .frame(width:17, height: 17)
                                        Text("*** **")
                                            .modifier(SemiBold(size: 11))
                                    }
                                    .foregroundColor(.white)
    
                                )
    
                                .frame(width:92, height: 28)
                        }
                    }
    
    
                }
                .onAppear{
                    chatViewModel.joinRoom(roomId: roomId)
                }
                .onChange(of: scenePhase, perform: { newScenePhase in
                    switch newScenePhase {
                    case .active:
                        print("App is active = chatview \(roomId)")
                        chatViewModel.setupSocket(){
                            chatViewModel.joinRoom(roomId: roomId)
                        }
                    case .inactive, .background:
                        print("App is in inactive or background = chatview")
                        chatViewModel.disconnectSocket()
                    @unknown default:
                        print("Unknown")
                    }
                })
    
                .navigationBarBackButtonHidden(true)
                .toolbar(tabBarVisible ? .visible : .hidden, for: .tabBar)
                .toolbar(showPaymentAlert ? .hidden : .visible, for: .navigationBar)
                .padding(.horizontal,22)  // VSTACK
                if showPaymentAlert{
                    ChatPaymentConfirmView(showPaymentAlert: $showPaymentAlert, roomId: $roomId, paymentSucceed: $paymentSucceed)
                }
    
            }
    
    
        }
    
    }

    뷰의 구조

    SwiftUI 내부 채팅 구조는
    VStack이 아닌 LazyVStack으로 채팅뷰를 구현했습니다.
    이유는 이러합니다

    • 성능 최적화: LazyVStack은 그 내부의 뷰들을 "게으르게" 로딩합니다. 즉, 해당 뷰가 화면에 실제로 표시될 때까지 뷰의 생성과 렌더링을 지연시킵니다. 이러한 방식은 메시지 목록과 같이 크고 동적인 목록을 표시할 때 메모리와 CPU를 절약하게 됩니다.
    • 동적 컨텐츠 처리: 채팅 환경에서는 메시지가 계속해서 추가됩니다. LazyVStack을 사용하면 새로운 메시지가 추가될 때마다 전체 메시지 목록을 다시 로드할 필요가 없습니다. 오직 새로 추가된 메시지만 로드되므로, 앱의 반응성이 향상됩니다.
    • 스크롤 관리: LazyVStack은 ScrollView와 함께 사용될 때 최적의 성능을 발휘합니다. 채팅 애플리케이션에서는 대개 많은 수의 메시지가 스크롤 가능한 목록으로 표시되므로, LazyVStack은 이러한 환경에서 자연스럽게 잘 작동합니다.
    • 데이터 로딩의 유연성: LazyVStack을 사용하면 데이터를 비동기적으로 로드하면서도 사용자에게 빠르게 UI를 표시할 수 있습니다. 이를 통해 앱이 더욱 반응적으로 느껴집니다.

    1. 함수별 기능 설명

    • buttonBack: 뒤로 가기 버튼의 기능을 정의합니다. 채팅방을 나갈 때와 탭바의 표시를 변경할 때 사용됩니다.
    • body: 이 부분에서 주요 뷰 구성 요소와 뷰의 행동이 정의됩니다.
    • ChatMessageRow: 각 메시지를 표시하는 뷰입니다.
    struct ChatMessageRow: View {
        @EnvironmentObject var appState:NavigationPathViewModel
        @EnvironmentObject var userInformationViewModel: UserInformationViewModel
        let message: ChatMessage
        let profileImage: String
        let lobbyistId: String
        var body: some View {
            HStack (alignment: .bottom) {
                if message.isCurrentUser {
                    Spacer()
                    if message.createdTime != "sending"{
                        Text("\(convertToHourAndMinute(dateString:message.createdTime))")
                            .modifier(Medium(size: 10))
                    }else{
                        Image("chat_dark")
                            .resizable()
                            .frame(width: 10,height: 10)
                    }
    
                    // 메시지의 타입에 따라 분기 처리를 합니다.
                    switch message.messageType {
                    case .text:
                        Text(message.text)
                            .padding()
                            .modifier(Regular(size: 14))
                            .background(Color("vivid_grey"))
                            .foregroundColor(.black)
                            .cornerRadius(10)
                    case .review:
                        RoundedRectangle(cornerRadius: 15)
                            .foregroundColor(Color("chat_background"))
                            .overlay(
                                VStack(alignment:.leading){
                                    Text("****")
                                        .modifier(Bold(size: 18))
                                        .padding(.bottom,24)
    
                                    Text("\(formatCurrency(value:message.price))원")
                                        .modifier(Bold(size: 18))
                                    Rectangle()
                                        .frame(width: 190, height:1)
    
                                    Text("*** *****!")
                                        .modifier(Regular(size: 10))
                                    Text("*** *** ****")
                                        .modifier(Regular(size: 10))
                                    Text("***** *** ** ******.")
                                        .modifier(Regular(size: 10))
                                    Spacer()
                                    NavigationLink(destination: {
                                        ReviewMainView(modes: $appState.reviewModes, lobbyistId: $userInformationViewModel.userInformation.id)
                                    }, label: {
                                        RoundedRectangle(cornerRadius: 5)
                                            .foregroundColor(.black)
                                            .overlay(
                                                Text("** ***")
                                                    .modifier(Bold(size: 15))
                                                    .foregroundColor(.white)
                                            )
                                            .frame(width: 190, height: 35)
                                    })
    
                                }
                                .padding(22)
                            )
    
                            .frame(width: 231, height: 240)
                    }
    
                } else {
    
                    switch message.messageType {
                    case .text:
                        WebImage(url: URL(string: "\(SHAREVAR.IMAGE_URL)/\(profileImage)"))
                            .resizable()
                            .scaledToFill()
                            .frame(width: 24, height: 24)
                            .clipped()
                            .cornerRadius(5)
    
                        Text(message.text)
                            .padding()
                            .modifier(Regular(size: 14))
                            .background(Color.white)
                            .foregroundColor(.black)
                            .cornerRadius(10)
                            .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray, lineWidth: 1))
                        Text("\(convertToHourAndMinute(dateString:message.createdTime))")
                            .modifier(Medium(size: 10))
                    case .review:
                            EmptyView()
                    }
    
    
    
                    Spacer()
                }
            }
        }
    }

    2. 뷰 계층에서의 사용

    • ScrollViewReader를 쓰는 이유: ScrollViewReader는 스크롤 뷰 내의 특정 위치로 스크롤하는 기능을 제공합니다. 새 메시지가 추가될 때마다 스크롤뷰를 최하단으로 자동 이동시키기 위해 사용됩니다.
    • WebImage를 사용하는 이유: WebImage는 SDWebImageSwiftUI의 부분으로 웹에서 이미지를 비동기적으로 로드하고 캐시합니다. 이를 통해 프로필 이미지와 같은 웹 기반 이미지를 효과적으로 로드하고 표시할 수 있습니다.

    질문이 있으시다면 댓글로 알려주시면 최대한 설명 해드릴 수 있도록 하겠습니다. 아 물론 코드 관련한 테클도 환영입니다.

    반응형