본문 바로가기

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

01. 인터페이스 디자인

더미 데이터


import Foundation

struct Message: Identifiable, Codable {
	var id: String
	var text: String
	var received: Bool
	var timestamp: Date
}

실제로 Firebase에 연결되지 않은 상태이기 때문에,
화면에 출력할 더미 데이터들이 필요하다.

더미데이터들은 Message 구조체로 이뤄져 있고,
해당 구조체는 열거와 형식 변환이 자유롭도록 Identifiable 프로토콜과 Codable 프로토콜을 채용한다.

 

TitleRow


TitleBar는 HStack을 사용해 3개의 덩어리로 구성돼 있다.
HStack의 spacing 파라미터에 20을 전달해 Embed 되는 View들의 간격을 20pt로 지정했다.

  • AsyncImage
    프로필 사진을 구현한다.
    Asset에 추가하지 않고 AsyncImage를 사용해 View가 생성되는 시점에 사진을 다운로드한다.
    AsyncImage는 Image 취급을 하지 않기 때문에 Closure에서 modifier를 사용해야 한다.
    적절한 cornerRadius를 추가해 둥근 모서리를 표현한다.
  • VStack
    사용자의 이름과 현재 상태를 나타낸다.
    두 개의 Text로 돼 있으며, 이름은 title, bold로 상태는 caption, gray로 표현했다.
    VStack의 frame modifier의 alignment를 leading으로 설정해 국가 설정에 따라 시작 지점에 맞춰지도록 했다.
    maxWidth를 infinity로 설정하면 화면에 표시할 수 있는 최대 너비로 View를 표시한다.
  • Image
    systemImage(SFSymbol)을 사용해 전화 아이콘을 표현했다.

HStack 전체는 기본 여백을 주기 위해 padding을 추가했다.
background 자체는 해당 View에 추가하지 않았다.
전화 아이콘의 경계를 보기 위해 Simulator에 background를 추가하는 방법이 있다.

 

MessageBubble


메시지에 따라 수신된 메시지는 왼쪽에 정렬되고, 발신된 메시지는 오른쪽에 정렬하도록 구현한다.
정렬뿐만이 아닌 색으로 구별할 수 있도록 예외처리를 적극 활용한다.

VStack을 사용해 말풍선 아래에 전송된 시간을 표시하도록 구현한다.

  • VStack
    말풍선과 시간이 수직으로 표시되도록 VStack을 사용한다.
    파라미터의 alignment는 표시하는 메시지의 flag(reveived)에 따라 leading과 trailing으로 구분돼 시간의 정렬 기준을 설정한다.
    frame modifier의 maxWidth는 infinity로 화면 너비 전체를 채우고, alignment를 flag로 분기해 말풍선의 정렬 기준을 설정한다.
  • HStack
    Text 하나를 embed 한다 해당 Text는 메시지의 내용을 표시하고, flag에 따라 background를 회색 혹은 노란색으로 표시한다.
    적절한 cornerRadius를 추가한다.
    frame modifier로 최대 너비를 설정하고(정렬로 수발신을 구분하기 때문에 infinity일 필요는 없다.) flag에 따라 alignment를 분기한다.
  • Text
    메세지의 timestamp를 사용해 시간을 표시한다.
    font는 caption2
    색은 회색으로 변경했다.
    메시지의 flag에 따라 leading이나 trailing에 25pt의 padding을 추가한다.

VStack에 Gesture를 하나 추가한다.
DragGesture의 minimumDistance를 사용해 홀딩 제스처를 구현하고,
해당 제스처의 상태에 따라 시간의 표시 여부를 변경한다.

preview에는 임의의 Message 객체를 만들어 전달한다.
id와 text, received, timestamp가 필요하다.

 

MessageField


메시지를 입력하기 위한 Custom TextField를 구현한다.
Button과 TextField를 횡으로 배치하기 위해 HStack을 사용한다.

  • HStack
    padding은 horizontal에 기본값을, vertical에 10을 지정했다.
    background는 Gray
    적당한 cornerRadius를 추가할 수도 있다.
  • Button
    action에 전송 기능을 구현한다.
    전송을 하고 나면 TextField를 초기화한다.

 

Custom TextField


    public init(_ titleKey: LocalizedStringKey, text: Binding<String>, onEditingChanged: @escaping (Bool) -> Void, onCommit: @escaping () -> Void)

    /// Creates a text field with a text label generated from a localized title
    /// string.
    ///
    /// - Parameters:
    ///   - titleKey: The key for the localized title of the text field,
    ///     describing its purpose.
    ///   - text: The text to display and edit.
    ///   - onEditingChanged: The action to perform when the user
    ///     begins editing `text` and after the user finishes editing `text`.
    ///     The closure receives a Boolean value that indicates the editing
    ///     status: `true` when the user begins editing, `false` when they
    ///     finish.

사용한 생성자는 위와 같다.

  • titleKey
    text field의 제목, 목적을 설명함
  • text
    표시하거나 편집할 text
  • onEditingChanged
    사용자가 text를 수정하거나 마칠 때 변경되는 Boolean
  • status
    편집을 시작하면 true, 끝나면 flase

이를 사용한 CustomTextField의 코드는 다음과 같다.

struct CustomTextField: View {
    var placeholder: Text
    @Binding var text: String
    var editingChanged: (Bool) -> () = {_ in}
    var commit: () -> () = {}

    var body: some View {
        ZStack(alignment: .leading) {
            if text.isEmpty {
                placeholder
                    .opacity((0.5))
            }

            TextField("", text: $text, onEditingChanged: editingChanged, onCommit: commit)
        }
    }
}

파라미터들의 구현은 기존과 동일하게 사용한다.
시각적인 부분만 body에서 변경하는데, ZStack을 사용한다.

  • ZStack
    생성자 파라미터의 alignment는 leading으로 설정했다.
    입력창이 비어 있으면 전달받은 placeholder를 표시하고 투명도를 50%로 설정한다.

 

ContentView


위에서 생성한 TitleRow, MessageBubble, MessageField를 한 화면에 배치한다.
구현 코드는 다음과 같다.

struct ContentView: View {
    @StateObject var messagesManager = ["Hi there", "Hi, what's wrong with you", "????", "I said what's wrong with you"]

    var body: some View {
        VStack {
            VStack {
                TitleRow()

                    ScrollView {
                        ForEach(sampleMessage, id: \.self) { text in
                            MessageBubble(message: Message(id: "3333", text: text, received: true, timestamp: Date()))
                        }
                    }
                        .padding(.top, 10)
                        .background(.white)
                    .cornerRadius(30, corners: [.topLeft, .topRight])
            }
            .background(Color("Yellow"))

            MessageField()
        }
    }
}

TitleRow와 말풍선을 나열한 ScrollView를 VStack에 Embed 한다.
특별할 게 없어 보이는 이 코드에서 주목할 점은 ScrollView의 modifier인 cornerRadius다.

extension View {
	func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
		clipShape(RoundedCorner(radius: radius, corners: corners))
	}
}

struct RoundedCorner: Shape {
	var radius: CGFloat = .infinity
	var corners: UIRectCorner = .allCorners
	
	func path(in rect: CGRect) -> Path {
		let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
		return Path(path.cgPath)
	}
}

별도의 Extensions라는 파일로 구현된 이 modifier는 UIRectCorner를 통해 표시하는 UI의 모서리를 구분할 수 있도록 돼있다.

 

Round Specific Corners SwiftUI

I know you can use .cornerRadius() to round all the corners of a swiftUI view but is there a way to round only specific corners such as the top?

stackoverflow.com

여러 방식의 해결책이 있고, 해당 방식은 많은 사용자들의 추천을 받는 방식이다.
결과적으로 corners 파라미터에 UIRectCorner 배열을 전달해, 해당 모서리에만 적용하는 것이 가능해진다.

 

Gesture 최적화


ScrollView는 기본적으로 '화면을 쓸어내리는 이벤트를 처리'하는 View로,
해당 제스처는 말풍선에 적용한 DragGesture와 그 사용성이 겹친다.

		.onTapGesture {
			
		}
		.gesture(
			DragGesture(minimumDistance: 0)
				.onChanged({ _ in
					showTime = true
				})
				.onEnded({ _ in
					showTime = false
				})
		)

따라서 위와 같이 빈 onTapGesture를 앞에 추가해
간단하게 DragGesture의 인식 속도에 지연을 주는 것이 가능하다.

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

05. 더 나아가기  (0) 2022.10.15
04. Firebase에 쓰기  (0) 2022.10.15
03. Firebase 초기화 및 Swift에서 사용하기  (0) 2022.10.13
02. Firebase 연결하기  (0) 2022.10.13
00. 시작하며  (0) 2022.10.11