Skip to content

Instantly share code, notes, and snippets.

@Akhrameev
Created June 3, 2025 09:50
Show Gist options
  • Save Akhrameev/b210c16ce67587f1124fb06ba1e1c931 to your computer and use it in GitHub Desktop.
Save Akhrameev/b210c16ce67587f1124fb06ba1e1c931 to your computer and use it in GitHub Desktop.
UIKit Cat in SwiftUI for live preview
import SwiftUI
import UIKit
// 1. Custom UIKit View: Cat with Speech Bubble
class CatWithSpeechBubbleView: UIView {
var message: String = "UIKit ❤️ SwiftUI" {
didSet {
setNeedsDisplay()
}
}
var catColor: UIColor = UIColor(red: 0.36, green: 0.36, blue: 0.41, alpha: 1.0)
var earDetailColor: UIColor = UIColor(red: 0.929, green: 0.616, blue: 0.490, alpha: 1.0)
var eyeColor: UIColor = .white
var pupilColor: UIColor = UIColor(red: 0.208, green: 0.196, blue: 0.200, alpha: 1.0)
var noseColor: UIColor = UIColor(red: 0.918, green: 0.365, blue: 0.275, alpha: 1.0)
var speechBubbleColor: UIColor = .systemOrange
var speechBubbleTextColor: UIColor = .black
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else { return }
let availableHeightForCat = rect.height * 0.65
let catScale = min(rect.width * 0.8, availableHeightForCat) / 100.0
let catOffsetY = rect.height - (availableHeightForCat * 1.05)
context.saveGState()
context.translateBy(x: rect.midX - (50 * catScale), y: catOffsetY)
context.scaleBy(x: catScale, y: catScale)
drawCatHead(context: context)
drawEars(context: context)
drawEyes(context: context)
drawNoseAndMouth(context: context)
context.restoreGState()
drawSpeechBubble(withText: message, in: rect, catHeadTopY: catOffsetY, context: context)
}
// MARK: - Cat Drawing Parts
private func drawCatHead(context: CGContext) {
let headPath = UIBezierPath(ovalIn: CGRect(x: 10, y: 10, width: 80, height: 75))
catColor.setFill()
headPath.fill()
}
private func drawEars(context: CGContext) {
// Left Ear
let leftEarPath = UIBezierPath()
leftEarPath.move(to: CGPoint(x: 15, y: 29))
leftEarPath.addQuadCurve(to: CGPoint(x: 5, y: 0), controlPoint: CGPoint(x: 5, y: 10))
leftEarPath.addQuadCurve(to: CGPoint(x: 35, y: 15), controlPoint: CGPoint(x: 25, y: -5))
leftEarPath.close()
catColor.setFill()
leftEarPath.fill()
let innerLeftEarPath = UIBezierPath()
innerLeftEarPath.move(to: CGPoint(x: 18, y: 22))
innerLeftEarPath.addQuadCurve(to: CGPoint(x: 12, y: 8), controlPoint: CGPoint(x: 12, y: 15))
innerLeftEarPath.addQuadCurve(to: CGPoint(x: 30, y: 16), controlPoint: CGPoint(x: 22, y: 5))
earDetailColor.setFill()
innerLeftEarPath.fill()
// Right Ear
let rightEarPath = UIBezierPath()
rightEarPath.move(to: CGPoint(x: 85, y: 29))
rightEarPath.addQuadCurve(to: CGPoint(x: 95, y: 0), controlPoint: CGPoint(x: 95, y: 10))
rightEarPath.addQuadCurve(to: CGPoint(x: 65, y: 15), controlPoint: CGPoint(x: 75, y: -5))
rightEarPath.close()
catColor.setFill()
rightEarPath.fill()
let innerRightEarPath = UIBezierPath()
innerRightEarPath.move(to: CGPoint(x: 82, y: 22))
innerRightEarPath.addQuadCurve(to: CGPoint(x: 88, y: 8), controlPoint: CGPoint(x: 88, y: 15))
innerRightEarPath.addQuadCurve(to: CGPoint(x: 70, y: 16), controlPoint: CGPoint(x: 78, y: 5))
earDetailColor.setFill()
innerRightEarPath.fill()
}
private func drawEyes(context: CGContext) {
let leftEyePath = UIBezierPath(ovalIn: CGRect(x: 25, y: 30, width: 20, height: 25))
eyeColor.setFill()
leftEyePath.fill()
let leftPupilPath = UIBezierPath(ovalIn: CGRect(x: 30, y: 38, width: 10, height: 12))
pupilColor.setFill()
leftPupilPath.fill()
let rightEyePath = UIBezierPath(ovalIn: CGRect(x: 55, y: 30, width: 20, height: 25))
eyeColor.setFill()
rightEyePath.fill()
let rightPupilPath = UIBezierPath(ovalIn: CGRect(x: 60, y: 38, width: 10, height: 12))
pupilColor.setFill()
rightPupilPath.fill()
}
private func drawNoseAndMouth(context: CGContext) {
let nosePath = UIBezierPath()
nosePath.move(to: CGPoint(x: 50, y: 55))
nosePath.addLine(to: CGPoint(x: 45, y: 62))
nosePath.addLine(to: CGPoint(x: 55, y: 62))
nosePath.close()
noseColor.setFill()
nosePath.fill()
let mouthPath = UIBezierPath()
mouthPath.move(to: CGPoint(x: 40, y: 68))
mouthPath.addQuadCurve(to: CGPoint(x: 50, y: 72), controlPoint: CGPoint(x: 45, y: 73))
mouthPath.addQuadCurve(to: CGPoint(x: 60, y: 68), controlPoint: CGPoint(x: 55, y: 73))
pupilColor.setStroke()
mouthPath.lineWidth = 1.5
mouthPath.stroke()
}
// MARK: - Speech Bubble Drawing
private func drawSpeechBubble(withText text: String, in rect: CGRect, catHeadTopY: CGFloat, context: CGContext) {
let bubblePadding: CGFloat = 10
let cornerRadius: CGFloat = 8
let tailHeight: CGFloat = 10
let tailWidth: CGFloat = 15
let textAttributes: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 16, weight: .medium),
.foregroundColor: speechBubbleTextColor
]
let attributedString = NSAttributedString(string: text, attributes: textAttributes)
let textSize = attributedString.boundingRect(with: CGSize(width: rect.width - 2 * bubblePadding - 20, height: .greatestFiniteMagnitude),
options: .usesLineFragmentOrigin, context: nil).size
let bubbleWidth = textSize.width + 2 * bubblePadding
let bubbleHeight = textSize.height + 2 * bubblePadding
var bubbleOriginX = rect.midX - bubbleWidth / 2
let bubbleOriginY = catHeadTopY - bubbleHeight - tailHeight - 5
if bubbleOriginX < 5 { bubbleOriginX = 5 }
if bubbleOriginX + bubbleWidth > rect.width - 5 { bubbleOriginX = rect.width - 5 - bubbleWidth }
let bubbleRect = CGRect(x: bubbleOriginX, y: bubbleOriginY, width: bubbleWidth, height: bubbleHeight)
let bubblePath = UIBezierPath(roundedRect: bubbleRect, cornerRadius: cornerRadius)
let tailPath = UIBezierPath()
let tailTipX = rect.midX
tailPath.move(to: CGPoint(x: tailTipX - tailWidth / 2, y: bubbleRect.maxY))
tailPath.addLine(to: CGPoint(x: tailTipX, y: bubbleRect.maxY + tailHeight))
tailPath.addLine(to: CGPoint(x: tailTipX + tailWidth / 2, y: bubbleRect.maxY))
tailPath.close()
bubblePath.append(tailPath)
speechBubbleColor.setFill()
bubblePath.fill()
let textRect = CGRect(x: bubbleRect.origin.x + bubblePadding,
y: bubbleRect.origin.y + bubblePadding,
width: textSize.width,
height: textSize.height)
attributedString.draw(in: textRect)
}
public func updateMessageText(_ newText: String) {
self.message = newText
}
}
// 2. UIViewRepresentable Struct
struct CatViewRepresentable: UIViewRepresentable {
@Binding var message: String
func makeUIView(context: Context) -> CatWithSpeechBubbleView {
let catView = CatWithSpeechBubbleView()
return catView
}
func updateUIView(_ uiView: CatWithSpeechBubbleView, context: Context) {
uiView.updateMessageText(message)
}
}
// 3. SwiftUI ContentView for Demonstration
struct CatDemoContentView: View {
@State private var catMessage: String = "UIKit ❤️ SwiftUI"
var body: some View {
VStack {
Text("SwiftUI Controls:")
.font(.headline)
.padding(.top)
TextField("Cat's message", text: $catMessage)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(.horizontal)
Spacer()
CatViewRepresentable(message: $catMessage)
.frame(width: 250, height: 300)
.border(Color.gray)
Spacer()
}
.navigationTitle("UIKit Cat in SwiftUI")
}
}
// Preview Provider
struct CatDemoContentView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
CatDemoContentView()
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment