Created
June 3, 2025 09:50
-
-
Save Akhrameev/b210c16ce67587f1124fb06ba1e1c931 to your computer and use it in GitHub Desktop.
UIKit Cat in SwiftUI for live preview
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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