본문 바로가기

프로젝트/ChatApp ver.1 (w/Firebase)

05. 더 나아가기

자동 스크롤


메시지의 양이 많아한 화면에 표시하지 못하는 경우 Scroll View는 화면 밖에 새로운 메시지를 표시한다.
카톡이나 기본 메시지 앱의 경우 새 메시지가 화면 밖에 표시되면 자동으로 맨 아래로 이동하게 되는데,
이를 구현해 본다.

 

Apple Developer Documentation

 

developer.apple.com

원리는 간단하다.
Scroll View의 scrollTo(_:anchor:) 메서드를 사용하는 것으로,
파라미터로 대상을 전달하기만 하면 된다.

struct ContentView: View {
	@StateObject var messagesManager = MessagesManager()
	
    var body: some View {
		VStack {
			VStack {
				TitleRow()
				
				ScrollViewReader { proxy in
					ScrollView {
						ForEach(messagesManager.messages, id: \.id) { message in
							MessageBubble(message: message)
						}
					}
					.padding(.top, 10)
					.background(.white)
					.cornerRadius(30, corners: [.topLeft, .topRight])
				}
			}
			.background(Color("Yellow"))
			
			MessageField()
				.environmentObject(MessagesManager())
		}
		.onTapGesture {
			hideKeyboard()
		}
    }
}

ContentView로 돌아와 ScrollView를 ScrollViewReader에 embed 해 준다.
ScrollViewReader는 proxy라는 것을 반환하게 되는데, 이를 사용해야 한다.

class MessagesManager: ObservableObject {
	@Published private(set) var messages: [Message] = []
	@Published private(set) var lastMessageId = ""
	let db = Firestore.firestore()
	
	init() {
		getMessages()
	}
	
	func getMessages() {
		db.collection("messages").addSnapshotListener { querySnapshot, error in
			guard let documents = querySnapshot?.documents else {
				print("Error fetching documents: \(String(describing: error))")
				return
			}
			
			self.messages = documents.compactMap { document -> Message? in
				do {
					return try document.data(as: Message.self)
				} catch {
					print("Error decoding document into Message: \(error)")
					return nil
				}
			}
			
			self.messages.sort { $0.timestamp < $1.timestamp }
			
			if let id = self.messages.last?.id {
				self.lastMessageId = id
			}
		}
	}
	
	func sendMessage(text: String) {
		do {
			let newMessage = Message(id: "\(UUID())", text: text, received: false, timestamp: Date())
			
			try db.collection("messages").document().setData(from: newMessage)
		} catch {
			print("Error adding message to Firestore: \(error)")
		}
	}
}

이제는 어디로 이동해야 하는지 대상을 전달해 줘야 한다.
핵심은 두 부분이다.

class MessagesManager: ObservableObject {
	@Published private(set) var messages: [Message] = []
	@Published private(set) var lastMessageId = ""
	let db = Firestore.firestore()

우선 대상이 될 '마지막 메시지'를 저장할 인스턴스를 하나 생성한다.

			
			self.messages.sort { $0.timestamp < $1.timestamp }
			
			if let id = self.messages.last?.id {
				self.lastMessageId = id
			}
		}
	}

그 다음 '메시지의 정렬이 완료된 이후'에 마지막 메시지의 id를 해당 변수에 전달하면 된다.

				ScrollViewReader { proxy in
					ScrollView {
						ForEach(messagesManager.messages, id: \.id) { message in
							MessageBubble(message: message)
						}
					}
					.padding(.top, 10)
					.background(.white)
					.cornerRadius(30, corners: [.topLeft, .topRight])
					.onChange(of: messagesManager.lastMessageId) { id in
						withAnimation {
							proxy.scrollTo(id, anchor: .bottom)
						}
					}
				}

다시 ContentView로 돌아와 ScrollView의 modifier로 'onChange'를 추가한다.
해당 modifier는 파라미터로 전달되는 대상이 변할 때마다 전달된 클로저를 실행하게 된다.

이제 새로운 메시지가 도착하면 자동으로 맨 아래로 내려가게 된다.
해당 방식은 서버를 감시하고 있는 상태에서 '서버에 메시지가 추가되는 순간' 마지막 메시지의 id를 갱신하게 되므로,
수신이건 발신이건 상관 없이 모두 작동하게 된다.

 

키보드 숨기기


메시지를 작성하다 키보드를 숨기고 채팅창을 살펴야 할 때가 있다.

지금은 enter를 눌러 키보드를 숨길 수 있지만,
해당 키를 전송 키로 사용하거나, 줄 바꿈을 지원하게 된다면 키보드를 숨길 방법은 '전송' 밖에는 없다.

 

SwiftUI에서 키보드 숨기기

SwiftUI가 버전업 되면서 여러 기능이 추가되는 가운데 여전히 지원하지 않는 기능은 Responder에 관한 제어 권한이다. 별도의 변수를 생성해 이를 이용해 제어하는 경우가 많은데, 그런 거창한 거

chillog.page

해당 메서드를 그대로 호출하면 된다.

struct ContentView: View {
	@StateObject var messagesManager = MessagesManager()
	
    var body: some View {
		VStack {
			VStack {
				TitleRow()
				
				ScrollViewReader { proxy in
					ScrollView {
						ForEach(messagesManager.messages, id: \.id) { message in
							MessageBubble(message: message)
						}
					}
					.padding(.top, 10)
					.background(.white)
					.cornerRadius(30, corners: [.topLeft, .topRight])
					.onChange(of: messagesManager.lastMessageId) { id in
						withAnimation {
							proxy.scrollTo(id, anchor: .bottom)
						}
					}
				}
			}
			.background(Color("Yellow"))
			
			MessageField()
				.environmentObject(MessagesManager())
		}
		.onTapGesture {
			hideKeyboard()
		}
    }
}

#if canImport(UIKit)
extension View {
	func hideKeyboard() {
		UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
	}
}
#endif

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

ViewContent의 View를 한 번 터치하면 해당 메서드를 호출하고, 키보드는 화면에서 자연스럽게 사라진다.

 

'프로젝트 > ChatApp ver.1 (w/Firebase)' 카테고리의 다른 글

04. Firebase에 쓰기  (0) 2022.10.15
03. Firebase 초기화 및 Swift에서 사용하기  (0) 2022.10.13
02. Firebase 연결하기  (0) 2022.10.13
01. 인터페이스 디자인  (0) 2022.10.11
00. 시작하며  (0) 2022.10.11