// // LiveRoomView.swift // SodaLive // // Created by klaus on 2023/08/14. // import SwiftUI import Kingfisher import PopupView struct LiveRoomView: View { @State private var isShowingNewChat = false @State private var isShowPhotoPicker = false @State private var noticeViewHeight: CGFloat = UIFont.systemFontSize let columns = [ GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()) ] let chatColumns = [GridItem(.flexible())] @StateObject var keyboardHandler = KeyboardHandler() @StateObject var viewModel = LiveRoomViewModel() var body: some View { ZStack { Color.black.edgesIgnoringSafeArea(.all) VStack(spacing: 0) { LazyVStack(alignment: .leading, spacing: 0) { HStack(spacing: 6.7) { Text( UserDefaults.int(forKey: .userId) == viewModel.liveRoomInfo?.creatorId ? "라이브 종료": "나가기" ) .font(.custom(Font.medium.rawValue, size: 10)) .foregroundColor(Color(hex: "ff5c49")) .padding(.horizontal, 14.3) .padding(.vertical, 8.3) .overlay( RoundedRectangle(cornerRadius: 13.3) .stroke(Color(hex: "ff5c49"), lineWidth: 1) ) .onTapGesture { viewModel.isExpandNotice = false if let liveRoomInfo = viewModel.liveRoomInfo, liveRoomInfo.creatorId == UserDefaults.int(forKey: .userId) { viewModel.isShowLiveEndPopup = true } else { viewModel.isShowQuitPopup = true } } Spacer() Text(viewModel.isBgOn ? "배경 ON" : "배경 OFF") .font(.custom(Font.medium.rawValue, size: 10)) .foregroundColor(Color(hex: viewModel.isBgOn ? "9970ff" : "eeeeee")) .padding(.horizontal, 14.3) .padding(.vertical, 8.3) .overlay( RoundedRectangle(cornerRadius: 13.3) .stroke(Color(hex: viewModel.isBgOn ? "9970ff" : "bbbbbb"), lineWidth: 1) ) .onTapGesture { viewModel.isBgOn.toggle() } HStack(spacing: 4.7) { Image("ic_share") .resizable() .frame(width: 16, height: 16) } .padding(.horizontal, 14.3) .padding(.vertical, 6) .overlay( RoundedRectangle(cornerRadius: 13.3) .stroke(Color(hex: "bbbbbb"), lineWidth: 1) ) .onTapGesture { viewModel.shareRoom() } if let liveRoomInfo = viewModel.liveRoomInfo, liveRoomInfo.creatorId == UserDefaults.int(forKey: .userId) { HStack(spacing: 4.7) { Image("ic_edit") .resizable() .frame(width: 16, height: 16) } .padding(.horizontal, 14.3) .padding(.vertical, 6) .overlay( RoundedRectangle(cornerRadius: 13.3) .stroke(Color(hex: "bbbbbb"), lineWidth: 1) ) .onTapGesture { viewModel.isShowEditRoomInfoDialog = true } } } .padding(.horizontal, 13.3) if let liveRoomInfo = viewModel.liveRoomInfo { ZStack { VStack(alignment: .leading, spacing: 0) { HStack(spacing: 5.3) { if liveRoomInfo.isAdult { Text("19") .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color(hex: "e33621")) .padding(.horizontal, 5.3) .padding(.vertical, 3.3) .background(Color(hex: "601d14")) .cornerRadius(2.6) } Text(liveRoomInfo.title) .font(.custom(Font.bold.rawValue, size: 15.3)) .foregroundColor(Color(hex: "eeeeee")) .lineLimit(1) } .padding(.top, 16.7) .padding(.horizontal, 13.3) LiveRoomTopCreatorView( creatorId: liveRoomInfo.creatorId, nickname: liveRoomInfo.creatorNickname, profileImageUrl: liveRoomInfo.creatorProfileUrl, isFollowing: liveRoomInfo.isFollowing, onClickProfile: { if liveRoomInfo.creatorId != UserDefaults.int(forKey: .userId) { viewModel.getUserProfile(userId: liveRoomInfo.creatorId) } }, onClickFollow: { if $0 { viewModel.creatorUnFollow() } else { viewModel.creatorFollow() } } ) .padding(.top, 16.7) .padding(.horizontal, 13.3) Rectangle() .frame(height: 1) .foregroundColor(Color(hex: "909090").opacity(0.3)) .padding(.horizontal, 13.3) .padding(.top, 8) NotificationView(liveRoomInfo: liveRoomInfo) } if viewModel.isMute { Image("img_noti_mute") } } if viewModel.isShowNotice { HStack(alignment: .top, spacing: 8) { Text("[공지]") .font(.custom(Font.bold.rawValue, size: 11.3)) .foregroundColor(.white) .onTapGesture { viewModel.isExpandNotice.toggle() } if viewModel.isExpandNotice { VStack(spacing: 10) { TextView(text: liveRoomInfo.notice, dynamicHeight: $noticeViewHeight) .frame(height: viewModel.isExpandNotice ? noticeViewHeight : UIFont.systemFontSize) Text("닫기") .font(.custom(Font.light.rawValue, size: 11.3)) .foregroundColor(.white) .onTapGesture { viewModel.isExpandNotice = false } } } else { Text(liveRoomInfo.notice) .font(.custom(Font.light.rawValue, size: 11.3)) .foregroundColor(.white) .lineLimit(1) .onTapGesture { viewModel.isExpandNotice = true } } } .padding(.horizontal, 26.7) .padding(.vertical, 13.3) .frame(width: screenSize().width, alignment: .leading) .background(Color(hex: "3d2a6c")) .padding(.top, 10) .contentShape(Rectangle()) } if !viewModel.isSpeakerFold { HStack(spacing: 0) { Text("스피커") .font(.custom(Font.bold.rawValue, size: 12)) .foregroundColor(Color(hex: "eeeeee")) Spacer() } .padding(.top, 20) .padding(.horizontal, 23.3) LazyVGrid(columns: columns) { ForEach(liveRoomInfo.speakerList, id: \.self) { speaker in VStack(spacing: 6.7) { ZStack { KFImage(URL(string: speaker.profileImage)) .resizable() .scaledToFill() .frame(width: 46.7, height: 46.7, alignment: .top) .clipShape(Circle()) .overlay( Circle() .stroke( Color(hex: "9970ff"), lineWidth: viewModel.activeSpeakers.contains(UInt(speaker.id)) ? 3 : 0 ) ) if viewModel.muteSpeakers.contains(UInt(speaker.id)) { Image("ic_mute") .resizable() .frame(width: 46.7, height: 46.7) } VStack(alignment: .leading, spacing: 0) { HStack(spacing: 0) { Spacer() if liveRoomInfo.creatorId == speaker.id { Image("ic_crown") .resizable() .frame(width: 16.7, height: 16.7) } } Spacer() } } .frame(width: 46.7, height: 46.7) Text(speaker.nickname) .font(.custom(Font.light.rawValue, size: 12)) .foregroundColor(Color(hex: "bbbbbb")) .lineLimit(1) } .onTapGesture { viewModel.selectedProfile = speaker viewModel.isShowProfilePopup = true } } } .padding(.top, 16.7) .padding(.horizontal, 23.3) } } } .padding(.vertical, 16.7) .frame(width: screenSize().width) .background(Color(hex: "222222")) .cornerRadius(16.7, corners: [.topLeft, .topRight]) ZStack(alignment: .top) { ScrollViewReader { proxy in ZStack(alignment: .bottomTrailing) { GeometryReader { proxy in if let coverImageUrl = viewModel.coverImageUrl, viewModel.isBgOn { KFImage(URL(string: coverImageUrl)) .resizable() .scaledToFill() .frame(width: proxy.size.width, height: proxy.size.height, alignment: .center) .clipped() Color.black.opacity(0.4) } } VStack(alignment: .leading, spacing: 0) { ScrollView(.vertical, showsIndicators: false) { scrollObservableView ChatView() .frame(width: screenSize().width) } .rotationEffect(Angle(degrees: 180)) .onTapGesture { hideKeyboard() } .onPreferenceChange(ScrollOffsetKey.self) { viewModel.setOffset($0) } InputChatView { isShowingNewChat = false proxy.scrollTo(viewModel.messages.count - 1, anchor: .center) }.padding(.bottom, 10) } VStack(spacing: 13.3) { if let liveRoomInfo = viewModel.liveRoomInfo { if liveRoomInfo.creatorId == UserDefaults.int(forKey: .userId) { Image("ic_roulette_settings") .resizable() .frame(width: 26.7, height: 26.7) .padding(11) .background(Color(hex: "525252").opacity(0.6)) .cornerRadius(10) .padding(.bottom, 13.3) .onTapGesture { viewModel.isShowRouletteSettings = true } } else if liveRoomInfo.creatorId != UserDefaults.int(forKey: .userId) && viewModel.isActiveRoulette { Image("ic_roulette") .resizable() .frame(width: 26.7, height: 26.7) .padding(11) .background(Color(hex: "525252").opacity(0.6)) .cornerRadius(10) .padding(.bottom, 13.3) .onTapGesture { viewModel.showRoulette() } } } if viewModel.role == .SPEAKER { Image(viewModel.isMute ? "ic_mic_off" : "ic_mic_on") .resizable() .frame(width: 26.7, height: 26.7) .padding(11) .background(Color(hex: "525252").opacity(0.6)) .cornerRadius(10) .padding(.bottom, 13.3) .onTapGesture { viewModel.toggleMute() } } Image(viewModel.isSpeakerMute ? "ic_speaker_off" : "ic_speaker_on") .resizable() .frame(width: 26.7, height: 26.7) .padding(11) .background(Color(hex: "525252").opacity(0.6)) .cornerRadius(10) .padding(.bottom, 13.3) .onTapGesture { viewModel.toggleSpeakerMute() } if let liveRoomInfo = viewModel.liveRoomInfo, liveRoomInfo.creatorId == UserDefaults.int(forKey: .userId) && UserDefaults.string(forKey: .role) == MemberRole.CREATOR.rawValue { Image("ic_donation_message_list") .resizable() .frame(width: 26.7, height: 26.7) .padding(11) .background(Color(hex: "525252").opacity(0.6)) .cornerRadius(10) .onTapGesture { viewModel.isShowDonationMessagePopup = true } } else { Image("ic_donation") .resizable() .frame(width: 26.7, height: 26.7) .padding(11) .background(Color(hex: "525252").opacity(0.6)) .cornerRadius(10) .onTapGesture { viewModel.isShowDonationPopup = true } } } .padding(.trailing, 16.7) .padding(.bottom, 85) if isShowingNewChat { NewChatView{ isShowingNewChat = false proxy.scrollTo(viewModel.messages.count - 1, anchor: .center) }.padding(.bottom, 70) } } .frame(width: screenSize().width) } HStack(spacing: 0) { Spacer() HStack(spacing: 6.7) { Image(viewModel.isSpeakerFold ? "ic_live_detail_bottom" : "ic_live_detail_top") .resizable() .frame(width: 20, height: 20) Text(viewModel.isSpeakerFold ? "펼치기" : "접기") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "bbbbbb")) } .padding(.vertical, 6.7) .padding(.horizontal, 13.3) .background(Color(hex: "222222")) .cornerRadius(10, corners: [.bottomLeft, .bottomRight]) .onTapGesture { viewModel.isSpeakerFold.toggle() } } .background( LinearGradient( gradient: Gradient(colors: [Color(hex: "222222").opacity(0.95), Color.black.opacity(0.005)]), startPoint: .top, endPoint: .bottom ).ignoresSafeArea() ) } } .popup(isPresented: $viewModel.isShowErrorPopup, type: .toast, position: .top, autohideIn: 1.3) { GeometryReader { geo in HStack { Spacer() Text(viewModel.errorMessage) .padding(.vertical, 13.3) .frame(width: geo.size.width - 66.7, alignment: .center) .font(.custom(Font.medium.rawValue, size: 12)) .background(Color(hex: "9970ff")) .foregroundColor(Color.white) .multilineTextAlignment(.center) .cornerRadius(20) .padding(.top, 66.7) Spacer() } .onDisappear { viewModel.quitRoom() } } } .cornerRadius(16.7, corners: [.topLeft, .topRight]) .offset(y: -(keyboardHandler.keyboardHeight > 0 ? keyboardHandler.keyboardHeight : 0)) .onAppear { UIApplication.shared.isIdleTimerDisabled = true UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) viewModel.getMemberCan() viewModel.initAgoraEngine() viewModel.getRoomInfo() NotificationCenter.default.addObserver( forName: UIApplication.willTerminateNotification, object: nil, queue: .main) { _ in viewModel.quitRoom() sleep(3) } } .onDisappear { UIApplication.shared.isIdleTimerDisabled = false NotificationCenter.default.removeObserver(self) } ZStack { if viewModel.isShowProfilePopup, let liveRoomInfo = viewModel.liveRoomInfo, let selectedProfile = viewModel.selectedProfile { LiveRoomProfileDialog( isShowing: $viewModel.isShowProfilePopup, profileInfo: selectedProfile, creatorId: liveRoomInfo.creatorId, isSpeaker: viewModel.role == .SPEAKER, onClickInviteSpeaker: { inviteSpeaker(peerId: $0) }, onClickChangeListener: { if $0 == UserDefaults.int(forKey: .userId) { viewModel.setListener() return } viewModel.changeListener(peerId: $0) } ) } if viewModel.isShowDonationPopup { LiveRoomDonationDialogView(isShowing: $viewModel.isShowDonationPopup, isAudioContentDonation: false) { can, message in viewModel.donation(can: can, message: message) } } if viewModel.isShowQuitPopup { SodaDialog( title: "라이브 나가기", desc: "라이브에서 나가시겠습니까?", confirmButtonTitle: "예", confirmButtonAction: { viewModel.isShowQuitPopup = false viewModel.quitRoom() }, cancelButtonTitle: "아니오", cancelButtonAction: { viewModel.isShowQuitPopup = false } ) } if viewModel.isShowLiveEndPopup { SodaDialog( title: "라이브 종료", desc: "라이브를 종료하시겠습니까?\n" + "라이브를 종료하면 대화내용은\n" + "저장되지 않고 사라집니다.\n" + "참여자들 또한 라이브가 종료되어\n" + "강제퇴장 됩니다.", confirmButtonTitle: "예", confirmButtonAction: { viewModel.isShowLiveEndPopup = false viewModel.quitRoom() }, cancelButtonTitle: "아니오", cancelButtonAction: { viewModel.isShowLiveEndPopup = false } ) } } ZStack { if viewModel.isShowProfileList, let liveRoomInfo = viewModel.liveRoomInfo { LiveRoomProfilesDialogView( isShowing: $viewModel.isShowProfileList, viewModel: viewModel, roomInfo: liveRoomInfo, registerNotification: { viewModel.creatorFollow() }, unRegisterNotification: { viewModel.creatorUnFollow() }, onClickProfile: { if $0 != UserDefaults.int(forKey: .userId) { viewModel.getUserProfile(userId: $0) } }, onClickNoChatting: { userId, nickname, profileUrl in viewModel.noChattingUserId = userId viewModel.noChattingUserNickname = nickname viewModel.noChattingUserProfileUrl = profileUrl viewModel.isShowNoChattingConfirm = true } ) } if viewModel.isShowUserProfilePopup, let userProfile = viewModel.userProfile { Color.black.opacity(0.7) .edgesIgnoringSafeArea(.all) LiveRoomUserProfileDialogView( isShowing: $viewModel.isShowUserProfilePopup, viewModel: viewModel, userProfile: userProfile, onClickSetManager: { viewModel.setManagerMessageToPeer(userId: $0) viewModel.setManager(userId: $0) }, onClickReleaseManager: { viewModel.changeListener(peerId: $0, isFromManager: true) }, onClickFollow: { viewModel.creatorFollow(creatorId: $0, isGetUserProfile: true) }, onClickUnFollow: { viewModel.creatorUnFollow(creatorId: $0, isGetUserProfile: true) }, onClickInviteSpeaker: { inviteSpeaker(peerId: $0) }, onClickChangeListener: { viewModel.changeListener(peerId: $0) }, onClickMenu: { userId, userNickname, isBlocked in viewModel.reportUserId = userId viewModel.reportUserNickname = userNickname viewModel.reportUserIsBlocked = isBlocked viewModel.isShowReportMenu = true }, onClickNoChatting: { userId, nickname, profileUrl in viewModel.noChattingUserId = userId viewModel.noChattingUserNickname = nickname viewModel.noChattingUserProfileUrl = profileUrl viewModel.isShowNoChattingConfirm = true } ) .padding(20) .popup(isPresented: $viewModel.isShowReportPopup, type: .toast, position: .top, autohideIn: 1.3) { GeometryReader { geo in HStack { Spacer() Text(viewModel.reportMessage) .padding(.vertical, 13.3) .frame(width: geo.size.width - 66.7, alignment: .center) .font(.custom(Font.medium.rawValue, size: 12)) .background(Color(hex: "9970ff")) .foregroundColor(Color.white) .multilineTextAlignment(.center) .cornerRadius(20) .padding(.top, 66.7) Spacer() } } } } if viewModel.isShowReportMenu { VStack(spacing: 0) { ProfileReportMenuView( isShowing: $viewModel.isShowReportMenu, isBlockedUser: viewModel.reportUserIsBlocked, userBlockAction: { viewModel.isShowUesrBlockConfirm = true }, userUnBlockAction: { viewModel.userUnBlock() }, userReportAction: { viewModel.isShowUesrReportView = true }, profileReportAction: { viewModel.isShowProfileReportConfirm = true } ) Rectangle() .foregroundColor(Color(hex: "222222")) .frame(width: screenSize().width, height: 15.3) } .ignoresSafeArea() } if viewModel.isShowUesrBlockConfirm { UserBlockConfirmDialogView( isShowing: $viewModel.isShowUesrBlockConfirm, nickname: viewModel.reportUserNickname, confirmAction: { viewModel.userBlock { userId in viewModel.kickOutId = userId viewModel.kickOut() } } ) } if viewModel.isShowUesrReportView { UserReportDialogView( isShowing: $viewModel.isShowUesrReportView, confirmAction: { reason in viewModel.report(type: .USER, reason: reason) } ) } if viewModel.isShowProfileReportConfirm { ProfileReportDialogView( isShowing: $viewModel.isShowProfileReportConfirm, confirmAction: { viewModel.report(type: .PROFILE) } ) } if viewModel.isShowNoChattingConfirm && viewModel.noChattingUserId > 0 { LiveRoomNoChattingDialogView( nickname: viewModel.noChattingUserNickname, profileUrl: viewModel.noChattingUserProfileUrl, confirmAction: { viewModel.isShowNoChattingConfirm = false viewModel.setNoChatting() }, cancelAction: { viewModel.noChattingUserId = 0 viewModel.noChattingUserNickname = "" viewModel.noChattingUserProfileUrl = "" viewModel.isShowNoChattingConfirm = false } ) } if viewModel.isShowPopup { LiveRoomDialogView( content: viewModel.popupContent, cancelTitle: viewModel.popupCancelTitle, cancelAction: viewModel.popupCancelAction, confirmTitle: viewModel.popupConfirmTitle, confirmAction: viewModel.popupConfirmAction ).onAppear { if viewModel.popupConfirmTitle == nil && viewModel.popupConfirmAction == nil { DispatchQueue.main.asyncAfter(deadline: .now() + 2) { viewModel.isShowPopup = false viewModel.popupCancelTitle = nil viewModel.popupCancelAction = nil viewModel.popupConfirmTitle = nil viewModel.popupConfirmAction = nil } } } } } if viewModel.isShowRouletteSettings { RouletteSettingsView(isShowing: $viewModel.isShowRouletteSettings) { isActiveRoulette in self.viewModel.setActiveRoulette(isActiveRoulette: isActiveRoulette) } } if let preview = viewModel.roulettePreview, viewModel.isShowRoulettePreview { RoulettePreviewDialog( isShowing: $viewModel.isShowRoulettePreview, title: nil, onClickSpin: { spinRoulette() }, preview: preview ) } if viewModel.isShowRoulette { RouletteViewDialog(isShowing: $viewModel.isShowRoulette, options: viewModel.rouletteItems, selectedOption: viewModel.rouletteSelectedItem) { viewModel.sendRouletteDonation() } } if viewModel.isLoading && viewModel.liveRoomInfo == nil { LoadingView() } } .ignoresSafeArea(.keyboard) .edgesIgnoringSafeArea(keyboardHandler.keyboardHeight > 0 ? .bottom : .init()) .sheet( isPresented: $viewModel.isShowShareView, onDismiss: { viewModel.shareMessage = "" }, content: { ActivityViewController(activityItems: [viewModel.shareMessage]) } ) .sheet(isPresented: $isShowPhotoPicker) { ImagePicker( isShowing: $isShowPhotoPicker, selectedImage: $viewModel.coverImage, sourceType: .photoLibrary ) } .sheet(isPresented: $viewModel.isShowEditRoomInfoDialog) { if let liveRoomInfo = viewModel.liveRoomInfo { LiveRoomInfoEditDialog( isShowing: $viewModel.isShowEditRoomInfoDialog, isShowPhotoPicker: $isShowPhotoPicker, viewModel: viewModel, isLoading: viewModel.isLoading, currentTitle: liveRoomInfo.title, currentNotice: liveRoomInfo.notice, coverImageUrl: liveRoomInfo.coverImageUrl, coverImage: viewModel.coverImage ) { newTitle, newNotice in self.viewModel.editLiveRoomInfo( title: newTitle, notice: newNotice ) } } else { EmptyView() .onAppear { viewModel.isShowEditRoomInfoDialog = false } } } .sheet(isPresented: $viewModel.isShowDonationRankingPopup) { LiveRoomDonationRankingDialog(isShowing: $viewModel.isShowDonationRankingPopup) } .sheet(isPresented: $viewModel.isShowDonationMessagePopup) { LiveRoomDonationMessageDialog(isShowing: $viewModel.isShowDonationMessagePopup) } } func makeAttributedString(_ text: String) -> NSAttributedString { let attributedString = NSMutableAttributedString(string: text) let urlRegex = try! NSRegularExpression(pattern: "\\b(https?://\\S+\\b|www\\.\\S+\\b)") let matches = urlRegex.matches(in: text, options: [], range: NSRange(text.startIndex..., in: text)) for match in matches { let url = (text as NSString).substring(with: match.range) if let detectedURL = URL(string: url) { attributedString.addAttribute(.link, value: detectedURL, range: match.range) } } return attributedString } private func inviteSpeaker(peerId: Int) { if viewModel.liveRoomInfo!.speakerList.count <= 4 { viewModel.inviteSpeaker(peerId: peerId) self.viewModel.popupContent = "스피커 요청을 보냈습니다.\n잠시만 기다려 주세요." self.viewModel.isShowPopup = true } else { viewModel.popupContent = "스피커 정원을 초과했습니다." viewModel.isShowPopup = true } } private var scrollObservableView: some View { GeometryReader { proxy in let offsetY = proxy.frame(in: .global).origin.y Color.clear .preference( key: ScrollOffsetKey.self, value: offsetY ) .onAppear { viewModel.setOriginOffset(offsetY) } } .frame(height: 0) } struct ScrollOffsetKey: PreferenceKey { static var defaultValue: CGFloat = .zero static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value += nextValue() } } @ViewBuilder func NotificationView(liveRoomInfo: GetRoomInfoResponse) -> some View { HStack(spacing: 8) { Image( viewModel.isShowNotice ? "ic_notice_selected" : "ic_notice_normal" ) .contentShape(Rectangle()) .onTapGesture { viewModel.isShowNotice.toggle() } .padding(.trailing, 10) Spacer() HStack(spacing: 4.7) { Image("ic_can") .resizable() .frame(width: 16, height: 16) Text("\(viewModel.totalDonationCan)") .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color(hex: "bbbbbb")) } .padding(.horizontal, 11.5) .padding(.vertical, 5.3) .overlay( RoundedRectangle(cornerRadius: 12.8) .strokeBorder(lineWidth: 1) .foregroundColor(Color(hex: "eeeeee")) ) .contentShape(Rectangle()) .onTapGesture { viewModel.isShowDonationRankingPopup = true } if liveRoomInfo.creatorId == UserDefaults.int(forKey: .userId) { HStack(spacing: 0) { Text("참여자") .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color(hex: "bbbbbb")) Text("\(liveRoomInfo.participantsCount)") .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color(hex: "9970ff")) .padding(.leading, 6.7) } .padding(.horizontal, 11.5) .padding(.vertical, 7.3) .overlay( RoundedRectangle(cornerRadius: 12.8) .strokeBorder(lineWidth: 1) .foregroundColor(Color(hex: "eeeeee")) ) .contentShape(Rectangle()) .onTapGesture { viewModel.isShowProfileList = true } } } .padding(.top, 13.3) .padding(.horizontal, 13.3) } @ViewBuilder func NewChatView(scrollToBottom: @escaping () -> Void) -> some View { HStack(spacing: 0) { Spacer() HStack(spacing: 6.7) { Image("ic_bottom_white") Text("새로운 채팅") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "eeeeee")) } .padding(.vertical, 8) .padding(.horizontal, 13.3) .background(Color(hex: "555555").opacity(0.8)) .cornerRadius(16.7) .padding(.bottom, 13.3) .onTapGesture { scrollToBottom() } Spacer() } } @ViewBuilder func InputChatView(scrollToBottom: @escaping () -> Void) -> some View { HStack(spacing: 0) { TextField("채팅을 입력하세요", text: $viewModel.chatMessage) .autocapitalization(.none) .disableAutocorrection(true) .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "eeeeee")) .accentColor(Color(hex: "3bb9f1")) .keyboardType(.default) .padding(.horizontal, 13.3) .onTapGesture { if viewModel.isNoChatting { viewModel.popupContent = "\(viewModel.remainingNoChattingTime)초 동안 채팅하실 수 없습니다" viewModel.isShowPopup = true } } Spacer() Image("btn_message_send") .resizable() .frame(width: 35, height: 35) .padding(6.7) .onTapGesture { viewModel.sendMessage() scrollToBottom() } } .background(Color(hex: "232323")) .cornerRadius(10) .overlay( RoundedRectangle(cornerRadius: 10) .strokeBorder(lineWidth: 1) .foregroundColor(Color(hex: "eeeeee")) ) .padding(13.3) } @ViewBuilder func ChatView() -> some View { LazyVGrid(columns: chatColumns, alignment: .leading, spacing: 20) { ForEach(0.. (56.7 * 2) { isShowingNewChat = true } } } private func spinRoulette() { viewModel.spinRoulette() } } struct LiveRoomView_Previews: PreviewProvider { static var previews: some View { LiveRoomView() } } struct TextView: UIViewRepresentable { var text: String @Binding var dynamicHeight: CGFloat func makeUIView(context: Context) -> UITextView { let textView = UITextView() textView.text = text textView.isEditable = false textView.isScrollEnabled = true textView.backgroundColor = .clear textView.dataDetectorTypes = .link textView.font = UIFont(name: Font.light.rawValue, size: 11.3) textView.textColor = .white textView.textContainer.lineFragmentPadding = 0 textView.textContainerInset = .zero return textView } func updateUIView(_ uiView: UITextView, context: Context) { uiView.text = text DispatchQueue.main.async { let height = uiView.sizeThatFits(uiView.frame.size).height self.dynamicHeight = height > 500 ? 500 : height } } func makeCoordinator() -> Coordinator { Coordinator($dynamicHeight) } class Coordinator: NSObject, UITextViewDelegate { var dynamicHeight: Binding init(_ dynamicHeight: Binding) { self.dynamicHeight = dynamicHeight } func textViewDidChange(_ textView: UITextView) { DispatchQueue.main.async { self.dynamicHeight.wrappedValue = textView.sizeThatFits(textView.frame.size).height } } } }