Skip to content

Instantly share code, notes, and snippets.

@CodeSlicing
Last active August 3, 2020 07:51
Show Gist options
  • Select an option

  • Save CodeSlicing/f753cd311948690323da9569cb18fd97 to your computer and use it in GitHub Desktop.

Select an option

Save CodeSlicing/f753cd311948690323da9569cb18fd97 to your computer and use it in GitHub Desktop.
Demo showing anime inspired chat bubbles, completely in SwiftUI. Requires Xcode 12 and PureSwiftUI 2.0.0-beta-1
//
// AnimeStyleChatDemo.swift
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
// AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
//
// Created by Adam Fordyce on 02/08/2020.
// Copyright © 2020 Adam Fordyce. All rights reserved.
//
import SwiftUI
import PureSwiftUI
private let senders: [Sender] = [
Sender(initials: "C.S", color: pastelBlue, sfSymbol: .line_3_crossed_swirl_circle_fill),
Sender(initials: "T.C", color: pastelPurple, sfSymbol: .🍏applelogo),
Sender(initials: "C.F", color: pastelYellow, sfSymbol: .moon_stars_fill),
]
private let me = senders[0]
private let conversation: [ConversationElement] = [
// Are you coming with me to buy the new iPhone?
ConversationElement(message: "新しいiPhoneを購入するために同行しますか?", sender: senders[0]),
// No way! A monster is destroying the city. Maybe tomorrow?
ConversationElement(message: "ありえない! モンスターが街を破壊しています。 明日かな?", sender: senders[1]),
// I've been waiting for this for a year. A monster is not going to stop me. Let's go!
ConversationElement(message: "私はこれを1年間待っていました。 モンスターは私を止めるつもりはありません。 行こう!", sender: senders[0]),
//Come on, Tim. Do not be afraid!
ConversationElement(message: "さあ、ティム。 恐れることはありません!😘", sender: senders[2]),
//But I ordered it online. It will arrive sometime this morning. This is not 2015 anymore!
ConversationElement(message: "しかし、私はそれをオンラインで注文しました。 今朝いつか届きます。 これはもう2015年ではありません!", sender: senders[1]),
//Have you heard about tradition? Standing in the queue is part of the overall experience!
ConversationElement(message: "もちろんそうです! 何を考えていたのかわかりません。 買い物に行きましょう!", sender: senders[0]),
//And to celebrate at the Apple Store!
ConversationElement(message: "そして、Apple Storeで祝うために!🥳", sender: senders[2]),
//Of course! I am ridiculous. Let's go shopping!
ConversationElement(message: "もちろん! 私はばかげています。 買い物に行きましょう!👊🏻", sender: senders[1]),
// Great. See you soon!
ConversationElement(message: "素晴らしい。 また近いうちにお会いしましょう!", sender: senders[0]),
//See you there!
ConversationElement(message: "またね!", sender: senders[2]),
]
private let messageWidth = UIScreen.mainWidth * 0.75
private let messageBgColor: Color = Color(#colorLiteral(red: 0.8980160356, green: 0.8980196118, blue: 0.8897935748, alpha: 1))
private let pastelBlue = Color(#colorLiteral(red: 0.5147306404, green: 0.6360479803, blue: 1, alpha: 1))
private let pastelPurple = Color(#colorLiteral(red: 0.8205519227, green: 0.6074476553, blue: 1, alpha: 1))
private let pastelYellow = Color(#colorLiteral(red: 0.9457640112, green: 0.8853469327, blue: 0.6038223113, alpha: 1))
private let bgColor = Color(red: 0.165, green: 0.169, blue: 0.156)
private let backgroundPath = Path { path in
let hexagonSize: CGFloat = 5
let hexagonLayoutGuide = LayoutGuideConfig.polar(rings: 1, segments: 6)
let numColumns = UIScreen.mainWidthScaled(1 / hexagonSize)
let numRows = UIScreen.mainHeightScaled(1 / hexagonSize) / 0.9
var g = hexagonLayoutGuide.layout(in: CGRect(0, 0, hexagonSize, hexagonSize))
for row in 0...(numRows.asInt + 1) {
for column in 0...(numColumns.asInt + 1) {
let offset = row.isOdd ? 0 : hexagonSize * 0.5
let origin = CGPoint(hexagonSize * column + offset, hexagonSize * row * 0.9)
path.move(g[1, 0, origin: origin])
for segment in 1...g.yCount {
path.line(g[1, segment, origin: origin])
}
}
}
}
struct AnimeStyleChatApp: View {
var body: some View {
ZStack {
BackgroundView()
.edgesIgnoringSafeArea(.all)
ChatConversation()
}
.greedyFrame()
}
}
private struct ChatConversation: View {
var body: some View {
ScrollView(showsIndicators: false) {
VStack(spacing: 28) {
ForEach(0..<conversation.count) { row in
let element = conversation[row]
let isMe = element.sender == me
MessageRow(element: element, layoutDirection: isMe ? .rightToLeft : .leftToRight)
}
}
.vPadding(20)
}
.greedyFrame()
.hPadding(10)
}
}
private struct MessageRow: View {
@Environment(\.layoutDirection) private var systemLayoutDirection: LayoutDirection
let element: ConversationElement
let layoutDirection: LayoutDirection
var body: some View {
HStack(alignment: .bottom, spacing: 4) {
IconView(sender: element.sender)
.iconShadowStyle()
.padding(.leading, 10)
let initialsText = CaptionText(element.sender.initials, Font.Weight.black)
initialsText
.overlay(element.sender.color
.overlay(LinearGradient([.white, .black],
to: .bottom)
.blendMode(.multiply)
.opacity(0.6))
.mask(initialsText))
.iconShadowStyle()
HStack(alignment: .bottom, spacing: 0) {
MessageArrow(layoutDirection: layoutDirection)
.fillColor(messageBgColor)
.frame(15)
.messageShadowStyle()
.zIndex(layoutDirection == LayoutDirection.leftToRight ? 0 : 1)
CaptionText(element.message, .thin).vPadding(12).hPadding(8)
.messageStyle()
.environment(\.layoutDirection, systemLayoutDirection)
.padding(.trailing, 25)
.zIndex(layoutDirection == LayoutDirection.leftToRight ? 1 : 0)
}
Spacer()
}
.environment(\.layoutDirection, layoutDirection)
}
}
private let darkening = LinearGradient([
(.gray, 0),
(.white, 0.4)], to: .bottom)
private extension View {
func messageStyle() -> some View {
backgroundColor(messageBgColor)
.overlay(
darkening
.opacity(0.5)
.blur(5)
.drawingGroup()
.blendMode(.multiply)
)
.clipRoundedRectangleWithStroke(5, messageBgColor, lineWidth: 3)
.background(RoundedRectangle(5).messageShadowStyle())
}
func messageShadowStyle() -> some View {
shadowColor(.black, 2, offset: .point(5))
}
func iconShadowStyle() -> some View {
shadowColor(.black, 2, offset: .point(2))
}
}
private struct BackgroundView: View {
var body: some View {
backgroundPath
.fill(Color(white: 0.1))
.background(Color(white: 0.2))
.overlay(LinearGradient([Color.white, Color.white, Color.clear], to: .bottomTrailing)
.drawingGroup()
.blendMode(.softLight)
)
}
}
private let messageArrowLayoutConfig = LayoutGuideConfig.grid(columns: 10, rows: 10)
private struct MessageArrow: Shape {
let layoutDirection: LayoutDirection
func path(in rect: CGRect) -> Path {
var path = Path()
var g = messageArrowLayoutConfig.layout(in: rect).scaled(CGSize(layoutDirection == LayoutDirection.leftToRight ? 1 : -1, 1))
path.move(g[0, 4])
path.curve(g[g.xCount, 3], cp1: g[6, 5], cp2: g[g.xCount, 3], showControlPoints: false)
path.line(g[g.xCount, 8])
path.curve(g[0, 4], cp1: g[g.xCount - 2, 8], cp2: g[3, 7], showControlPoints: false)
return path
}
}
private struct IconView: View {
let sender: Sender
var body: some View {
let roundedRectangle = RoundedRectangle(5)
return Frame(30, sender.color)
.overlay(SFSymbol(sender.sfSymbol)
.resizedToFit().padding(3)
.foregroundColor(.gray)
.background(Color.white)
.overlay(
GlassShape()
.fillColor(.gray)
.blendMode(.colorBurn)
)
.drawingGroup()
.blendMode(.multiply)
)
.overlay(roundedRectangle
.strokeColor(.white, lineWidth: 1)
.frame(34)
.blur(0.5)
.offset(.point(2))
.blendMode(.softLight)
)
.overlay(roundedRectangle
.strokeColor(.white, lineWidth: 4)
.shadowColor(Color(white: 0.3), 3)
.blendMode(.multiply)
)
.overlay(roundedRectangle
.strokeColor(Color(white: 0.1), lineWidth: 4)
.blur(4)
.drawingGroup()
.blendMode(.multiply)
.mask(GlassShape()
.fillColor(.white)
)
)
.clipShape(roundedRectangle.inset(by: 0.6))
}
}
private struct GlassShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(rect.bottomLeading)
path.line(0, rect.heightScaled(0.65))
path.line(rect.maxX, rect.heightScaled(0.35))
path.line(rect.bottomTrailing)
path.closeSubpath()
return path
}
}
private struct ConversationElement {
let message: String
let sender: Sender
}
private struct Sender: Equatable {
let initials: String
let color: Color
let sfSymbol: SFSymbolName
}
struct AnimeStyleChatApp_Previews: PreviewProvider {
struct AnimeStyleChatApp_Harness: View {
var body: some View {
AnimeStyleChatApp()
}
}
static var previews: some View {
AnimeStyleChatApp_Harness()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment