在 SwiftUI 利用 Path 和 Shape 建立 iMessage 的對話框!
本篇原文(標題: SwiftUI: Creating a Chat Bubble (like iMessage) using Path and Shape)刊登於作者 Medium,由 Prafulla Singh 所著,並授權翻譯及轉載。
在這篇教學文章中,我們會學習建立 iMessage 那種有尾巴 (tail) 的對話框 (chat bubble)。這篇文章不是旨在建立一個端對端 (End-to-End) 的聊天 UI。讓我們先看看完成的範例:

實作邏輯
- 在 Shape Struct 中建立對話框的 Path。
- 這個 Shape 會以尾巴為參數,來定義形狀。
- 建立一個 Container View,這個視圖會包含訊息/圖像,並為訊息/圖像賦予形狀。
- 這個 Container View 也會管理視圖的方向,也就是向左或向右對齊對話框。
建立對話框 Path
雖然,我們可以直接寫程式碼來建立對話框 Path,但這次我們會用 Paintcodeapp 繪製 Shape,並即時進行測試。
要建立對話框 Path,我們先要是取得四個角,其中三個角設定為圓角,而剩下的一個角,就創建一個倒轉的心型。
struct ChatBubbleShape: Shape { enum Direction { case left case right } let direction: Direction func path(in rect: CGRect) -> Path { return (direction == .left) ? getLeftBubblePath(in: rect) : getRightBubblePath(in: rect) } private func getLeftBubblePath(in rect: CGRect) -> Path { let width = rect.width let height = rect.height let path = Path { p in p.move(to: CGPoint(x: 25, y: height)) p.addLine(to: CGPoint(x: width - 20, y: height)) p.addCurve(to: CGPoint(x: width, y: height - 20), control1: CGPoint(x: width - 8, y: height), control2: CGPoint(x: width, y: height - 8)) p.addLine(to: CGPoint(x: width, y: 20)) p.addCurve(to: CGPoint(x: width - 20, y: 0), control1: CGPoint(x: width, y: 8), control2: CGPoint(x: width - 8, y: 0)) p.addLine(to: CGPoint(x: 21, y: 0)) p.addCurve(to: CGPoint(x: 4, y: 20), control1: CGPoint(x: 12, y: 0), control2: CGPoint(x: 4, y: 8)) p.addLine(to: CGPoint(x: 4, y: height - 11)) p.addCurve(to: CGPoint(x: 0, y: height), control1: CGPoint(x: 4, y: height - 1), control2: CGPoint(x: 0, y: height)) p.addLine(to: CGPoint(x: -0.05, y: height - 0.01)) p.addCurve(to: CGPoint(x: 11.0, y: height - 4.0), control1: CGPoint(x: 4.0, y: height + 0.5), control2: CGPoint(x: 8, y: height - 1)) p.addCurve(to: CGPoint(x: 25, y: height), control1: CGPoint(x: 16, y: height), control2: CGPoint(x: 20, y: height)) } return path } private func getRightBubblePath(in rect: CGRect) -> Path { let width = rect.width let height = rect.height let path = Path { p in p.move(to: CGPoint(x: 25, y: height)) p.addLine(to: CGPoint(x: 20, y: height)) p.addCurve(to: CGPoint(x: 0, y: height - 20), control1: CGPoint(x: 8, y: height), control2: CGPoint(x: 0, y: height - 8)) p.addLine(to: CGPoint(x: 0, y: 20)) p.addCurve(to: CGPoint(x: 20, y: 0), control1: CGPoint(x: 0, y: 8), control2: CGPoint(x: 8, y: 0)) p.addLine(to: CGPoint(x: width - 21, y: 0)) p.addCurve(to: CGPoint(x: width - 4, y: 20), control1: CGPoint(x: width - 12, y: 0), control2: CGPoint(x: width - 4, y: 8)) p.addLine(to: CGPoint(x: width - 4, y: height - 11)) p.addCurve(to: CGPoint(x: width, y: height), control1: CGPoint(x: width - 4, y: height - 1), control2: CGPoint(x: width, y: height)) p.addLine(to: CGPoint(x: width + 0.05, y: height - 0.01)) p.addCurve(to: CGPoint(x: width - 11, y: height - 4), control1: CGPoint(x: width - 4, y: height + 0.5), control2: CGPoint(x: width - 8, y: height - 1)) p.addCurve(to: CGPoint(x: width - 25, y: height), control1: CGPoint(x: width - 16, y: height), control2: CGPoint(x: width - 20, y: height)) } return path } }
建立 Container View
這個 View 會以子內容 (Child Content) 和對話框尾巴的方向為參數。現在,我們使用 ‘clipShape’ 將 child view 切成一個對話框的樣子。
struct ChatBubble<Content>: View where Content: View { let direction: ChatBubbleShape.Direction let content: () -> Content init(direction: ChatBubbleShape.Direction, @ViewBuilder content: @escaping () -> Content) { self.content = content self.direction = direction } var body: some View { HStack { if direction == .right { Spacer() } content().clipShape(ChatBubbleShape(direction: direction)) if direction == .left { Spacer() } }.padding([(direction == .left) ? .leading : .trailing, .top, .bottom], 20) .padding((direction == .right) ? .leading : .trailing, 50) } }
完整範例
// // ChatBubble.swift // ios14-demo // // Created by Prafulla Singh on 25/7/20. // import SwiftUI struct ChatBubble<Content>: View where Content: View { let direction: ChatBubbleShape.Direction let content: () -> Content init(direction: ChatBubbleShape.Direction, @ViewBuilder content: @escaping () -> Content) { self.content = content self.direction = direction } var body: some View { HStack { if direction == .right { Spacer() } content().clipShape(ChatBubbleShape(direction: direction)) if direction == .left { Spacer() } }.padding([(direction == .left) ? .leading : .trailing, .top, .bottom], 20) .padding((direction == .right) ? .leading : .trailing, 50) } } struct ChatBubbleShape: Shape { enum Direction { case left case right } let direction: Direction func path(in rect: CGRect) -> Path { return (direction == .left) ? getLeftBubblePath(in: rect) : getRightBubblePath(in: rect) } private func getLeftBubblePath(in rect: CGRect) -> Path { let width = rect.width let height = rect.height let path = Path { p in p.move(to: CGPoint(x: 25, y: height)) p.addLine(to: CGPoint(x: width - 20, y: height)) p.addCurve(to: CGPoint(x: width, y: height - 20), control1: CGPoint(x: width - 8, y: height), control2: CGPoint(x: width, y: height - 8)) p.addLine(to: CGPoint(x: width, y: 20)) p.addCurve(to: CGPoint(x: width - 20, y: 0), control1: CGPoint(x: width, y: 8), control2: CGPoint(x: width - 8, y: 0)) p.addLine(to: CGPoint(x: 21, y: 0)) p.addCurve(to: CGPoint(x: 4, y: 20), control1: CGPoint(x: 12, y: 0), control2: CGPoint(x: 4, y: 8)) p.addLine(to: CGPoint(x: 4, y: height - 11)) p.addCurve(to: CGPoint(x: 0, y: height), control1: CGPoint(x: 4, y: height - 1), control2: CGPoint(x: 0, y: height)) p.addLine(to: CGPoint(x: -0.05, y: height - 0.01)) p.addCurve(to: CGPoint(x: 11.0, y: height - 4.0), control1: CGPoint(x: 4.0, y: height + 0.5), control2: CGPoint(x: 8, y: height - 1)) p.addCurve(to: CGPoint(x: 25, y: height), control1: CGPoint(x: 16, y: height), control2: CGPoint(x: 20, y: height)) } return path } private func getRightBubblePath(in rect: CGRect) -> Path { let width = rect.width let height = rect.height let path = Path { p in p.move(to: CGPoint(x: 25, y: height)) p.addLine(to: CGPoint(x: 20, y: height)) p.addCurve(to: CGPoint(x: 0, y: height - 20), control1: CGPoint(x: 8, y: height), control2: CGPoint(x: 0, y: height - 8)) p.addLine(to: CGPoint(x: 0, y: 20)) p.addCurve(to: CGPoint(x: 20, y: 0), control1: CGPoint(x: 0, y: 8), control2: CGPoint(x: 8, y: 0)) p.addLine(to: CGPoint(x: width - 21, y: 0)) p.addCurve(to: CGPoint(x: width - 4, y: 20), control1: CGPoint(x: width - 12, y: 0), control2: CGPoint(x: width - 4, y: 8)) p.addLine(to: CGPoint(x: width - 4, y: height - 11)) p.addCurve(to: CGPoint(x: width, y: height), control1: CGPoint(x: width - 4, y: height - 1), control2: CGPoint(x: width, y: height)) p.addLine(to: CGPoint(x: width + 0.05, y: height - 0.01)) p.addCurve(to: CGPoint(x: width - 11, y: height - 4), control1: CGPoint(x: width - 4, y: height + 0.5), control2: CGPoint(x: width - 8, y: height - 1)) p.addCurve(to: CGPoint(x: width - 25, y: height), control1: CGPoint(x: width - 16, y: height), control2: CGPoint(x: width - 20, y: height)) } return path } } struct Demo: View { var body: some View { ScrollView { VStack { ChatBubble(direction: .left) { Text("Hello!") .padding(.all, 20) .foregroundColor(Color.white) .background(Color.blue) } ChatBubble(direction: .right) { Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse ut semper quam. Phasellus non mauris sem. Donec sed fermentum eros. Donec pretium nec turpis a semper. ") .padding(.all, 20) .foregroundColor(Color.white) .background(Color.blue) } ChatBubble(direction: .right) { Image.init("dummyImage") .resizable() .frame(width: UIScreen.main.bounds.width - 70, height: 200).aspectRatio(contentMode: .fill) } } } } } struct ChatBubble_Previews: PreviewProvider { static var previews: some View { Demo() } }
本篇原文(標題:SwiftUI: Creating a Chat Bubble (like iMessage) using Path and Shape)刊登於作者 Medium,由 Prafulla Singh 所著,並授權翻譯及轉載。
作者簡介:Prafulla Singh,Block.one 的 iOS 開發者
譯者簡介:Kelly Chan-AppCoda 編輯小姐。
iOS
下一篇
應用 SwiftUI Path API 繪製撲克牌的四種花色!
iOS