iOS
iOS/Swift Integration
Native iOS app integration with Swift
Native iOSSwift 5.0+SwiftUI & UIKit
Quick Start
1. Project Setup
swift
Click to copy
// Add to your iOS project
// Minimum iOS version: 13.0
// Swift version: 5.0+
// In your Package.swift or Xcode Package Manager, add:
// No external dependencies required - using URLSession and Foundation
// Info.plist - Add internet permission
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
// Or for production, configure specific domains:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>api.calligraphymaker.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<false/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
</dict>
</dict>
</dict>2. API Service (Swift)
swift
Click to copy
// CalligraphyAPIService.swift
import Foundation
import UIKit
class CalligraphyAPIService {
static let shared = CalligraphyAPIService()
private let baseURL = "https://api.calligraphymaker.com/api/v2"
private let apiKey = "YOUR_API_KEY_HERE"
private init() {}
// MARK: - Data Models
struct GenerateRequest: Codable {
let text: String
let language: String
let fontStyle: String
let count: Int
let imageFormat: String
init(text: String, language: String = "hindi", fontStyle: String = "calligraphy", count: Int = 1, imageFormat: String = "png") {
self.text = text
self.language = language
self.fontStyle = fontStyle
self.count = count
self.imageFormat = imageFormat
}
}
struct CalligraphyResponse: Codable {
let success: Bool
let data: CalligraphyData?
}
struct CalligraphyData: Codable {
let results: [CalligraphyResult]
let metadata: CalligraphyMetadata
let usage: CalligraphyUsage
}
struct CalligraphyResult: Codable {
let id: String
let resultText: String
let fontFamily: String
let cdnUrl: String?
let appliedVariants: [AppliedVariant]
}
struct AppliedVariant: Codable {
let type: String
let position: Int
let character: String
}
struct CalligraphyMetadata: Codable {
let totalVariations: Int
let averageVariantsApplied: Double
let fontFamily: String
}
struct CalligraphyUsage: Codable {
let creditsUsed: Int
let remainingCredits: Int
enum CodingKeys: String, CodingKey {
case creditsUsed = "credits_used"
case remainingCredits = "remaining_credits"
}
}
struct SvgDownloadRequest: Codable {
let resultText: String
let fontName: String
}
struct SvgDownloadResponse: Codable {
let success: Bool
let data: SvgData?
}
struct SvgData: Codable {
let resultText: String
let fontFamily: String
let svgBase64: String
let svgString: String
let dimensions: Dimensions
}
struct ImageDownloadRequest: Codable {
let resultText: String
let fontName: String
let imageWidth: Int
let imageHeight: Int
let imageQuality: Int
let imageFormat: String
let backgroundColor: String
let textColor: String
}
struct ImageDownloadResponse: Codable {
let success: Bool
let data: ImageData?
}
struct ImageData: Codable {
let resultText: String
let fontName: String
let format: String
let imageBase64: String
let dimensions: Dimensions
let quality: Int
let backgroundColor: String
let textColor: String
}
struct Dimensions: Codable {
let width: Int
let height: Int
}
// MARK: - API Methods
func generateCalligraphy(
text: String,
language: String = "hindi",
fontStyle: String = "calligraphy",
count: Int = 1
) async throws -> CalligraphyData {
let request = GenerateRequest(
text: text,
language: language,
fontStyle: fontStyle,
count: count
)
let data = try await performRequest(
endpoint: "/generate",
method: "POST",
body: request
)
let response = try JSONDecoder().decode(CalligraphyResponse.self, from: data)
guard response.success, let calligraphyData = response.data else {
throw APIError.invalidResponse
}
return calligraphyData
}
func downloadSvg(resultText: String, fontName: String) async throws -> SvgData {
let request = SvgDownloadRequest(resultText: resultText, fontName: fontName)
let data = try await performRequest(
endpoint: "/download-svg",
method: "POST",
body: request
)
let response = try JSONDecoder().decode(SvgDownloadResponse.self, from: data)
guard response.success, let svgData = response.data else {
throw APIError.invalidResponse
}
return svgData
}
func downloadImage(
resultText: String,
fontName: String,
width: Int = 800,
height: Int = 300,
quality: Int = 90,
format: String = "png"
) async throws -> ImageData {
let request = ImageDownloadRequest(
resultText: resultText,
fontName: fontName,
imageWidth: width,
imageHeight: height,
imageQuality: quality,
imageFormat: format,
backgroundColor: "#ffffff",
textColor: "#000000"
)
let data = try await performRequest(
endpoint: "/download-image",
method: "POST",
body: request
)
let response = try JSONDecoder().decode(ImageDownloadResponse.self, from: data)
guard response.success, let imageData = response.data else {
throw APIError.invalidResponse
}
return imageData
}
// MARK: - Private Methods
private func performRequest<T: Codable>(
endpoint: String,
method: String,
body: T? = nil
) async throws -> Data {
guard let url = URL(string: baseURL + endpoint) else {
throw APIError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = method
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(apiKey, forHTTPHeaderField: "x-api-key")
if let body = body {
request.httpBody = try JSONEncoder().encode(body)
}
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
guard 200...299 ~= httpResponse.statusCode else {
throw APIError.httpError(httpResponse.statusCode)
}
return data
}
}
// MARK: - Error Handling
enum APIError: Error, LocalizedError {
case invalidURL
case invalidResponse
case httpError(Int)
case decodingError
case networkError
var errorDescription: String? {
switch self {
case .invalidURL:
return "Invalid URL"
case .invalidResponse:
return "Invalid response from server"
case .httpError(let code):
switch code {
case 401:
return "Invalid API key"
case 402:
return "Insufficient credits"
case 429:
return "Rate limit exceeded"
default:
return "HTTP Error: \(code)"
}
case .decodingError:
return "Failed to decode response"
case .networkError:
return "Network error occurred"
}
}
}3. SwiftUI Implementation
swift
Click to copy
// ContentView.swift
import SwiftUI
struct ContentView: View {
@StateObject private var viewModel = CalligraphyViewModel()
@State private var inputText = "नमस्ते दुनिया"
@State private var selectedLanguage = "hindi"
@State private var selectedFontStyle = "calligraphy"
@State private var generationCount = 3
let languages = ["hindi", "marathi", "gujarati", "english"]
let fontStyles = ["calligraphy", "decorative", "publication"]
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 20) {
// Header
VStack {
Text("Calligraphy Generator")
.font(.largeTitle)
.fontWeight(.bold)
Text("Generate beautiful calligraphy with AI")
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding()
// Input Section
VStack(alignment: .leading, spacing: 16) {
GroupBox("Input Text") {
TextEditor(text: $inputText)
.frame(minHeight: 100)
}
HStack {
GroupBox("Language") {
Picker("Language", selection: $selectedLanguage) {
ForEach(languages, id: \.self) { language in
Text(language.capitalized).tag(language)
}
}
.pickerStyle(MenuPickerStyle())
}
GroupBox("Font Style") {
Picker("Font Style", selection: $selectedFontStyle) {
ForEach(fontStyles, id: \.self) { style in
Text(style.capitalized).tag(style)
}
}
.pickerStyle(MenuPickerStyle())
}
}
GroupBox("Generation Count: \(generationCount)") {
Slider(value: .init(
get: { Double(generationCount) },
set: { generationCount = Int($0) }
), in: 1...5, step: 1)
}
Button(action: generateCalligraphy) {
HStack {
if viewModel.isLoading {
ProgressView()
.scaleEffect(0.8)
} else {
Image(systemName: "wand.and.rays")
}
Text("Generate Calligraphy")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.accentColor)
.foregroundColor(.white)
.cornerRadius(10)
}
.disabled(viewModel.isLoading || inputText.isEmpty)
}
.padding()
// Results Section
if !viewModel.results.isEmpty {
VStack(alignment: .leading) {
HStack {
Text("Generated Results (\(viewModel.results.count))")
.font(.headline)
Spacer()
if let usage = viewModel.usage {
Text("Credits: \(usage.remainingCredits)")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding(.horizontal)
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible())
], spacing: 16) {
ForEach(viewModel.results, id: \.id) { result in
CalligraphyResultCard(result: result) {
viewModel.selectedResult = result
viewModel.showingResultDetail = true
}
}
}
.padding(.horizontal)
}
}
// Error Display
if let error = viewModel.errorMessage {
Text(error)
.foregroundColor(.red)
.padding()
.background(Color.red.opacity(0.1))
.cornerRadius(8)
.padding(.horizontal)
}
}
}
.navigationBarHidden(true)
}
.sheet(isPresented: $viewModel.showingResultDetail) {
if let result = viewModel.selectedResult {
CalligraphyDetailView(result: result, viewModel: viewModel)
}
}
}
private func generateCalligraphy() {
Task {
await viewModel.generateCalligraphy(
text: inputText,
language: selectedLanguage,
fontStyle: selectedFontStyle,
count: generationCount
)
}
}
}
// MARK: - Result Card View
struct CalligraphyResultCard: View {
let result: CalligraphyResult
let onTap: () -> Void
var body: some View {
VStack {
AsyncImage(url: URL(string: result.cdnUrl ?? "")) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
} placeholder: {
Rectangle()
.fill(Color.gray.opacity(0.3))
.overlay(
ProgressView()
)
}
.frame(height: 120)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
VStack(alignment: .leading, spacing: 4) {
Text(result.resultText)
.font(.caption)
.fontWeight(.medium)
.lineLimit(2)
Text(result.fontFamily)
.font(.caption2)
.foregroundColor(.secondary)
Text("\(result.appliedVariants.count) variants")
.font(.caption2)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
.onTapGesture(perform: onTap)
}
}
// MARK: - Detail View
struct CalligraphyDetailView: View {
let result: CalligraphyResult
@ObservedObject var viewModel: CalligraphyViewModel
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
VStack(spacing: 20) {
AsyncImage(url: URL(string: result.cdnUrl ?? "")) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
} placeholder: {
ProgressView()
}
.frame(maxHeight: 300)
.background(Color.gray.opacity(0.1))
.cornerRadius(12)
VStack(alignment: .leading, spacing: 12) {
DetailRow(title: "Text", value: result.resultText)
DetailRow(title: "Font Family", value: result.fontFamily)
DetailRow(title: "Variants Applied", value: "\(result.appliedVariants.count)")
}
.padding()
.background(Color(.secondarySystemBackground))
.cornerRadius(12)
VStack(spacing: 12) {
Button("Download as SVG") {
Task {
await viewModel.downloadSvg(
resultText: result.resultText,
fontName: result.fontFamily
)
}
}
.buttonStyle(.borderedProminent)
Button("Download as Image") {
Task {
await viewModel.downloadImage(
resultText: result.resultText,
fontName: result.fontFamily
)
}
}
.buttonStyle(.bordered)
if let imageUrl = result.cdnUrl {
Button("Save to Photos") {
saveImageToPhotos(from: imageUrl)
}
.buttonStyle(.bordered)
}
}
Spacer()
}
.padding()
.navigationTitle("Calligraphy Details")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
dismiss()
}
}
}
}
}
private func saveImageToPhotos(from urlString: String) {
guard let url = URL(string: urlString) else { return }
Task {
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let image = UIImage(data: data) {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
} catch {
print("Failed to save image: \(error)")
}
}
}
}
struct DetailRow: View {
let title: String
let value: String
var body: some View {
HStack {
Text(title)
.fontWeight(.medium)
Spacer()
Text(value)
.foregroundColor(.secondary)
}
}
}ViewModel & State Management
CalligraphyViewModel.swift
swift
Click to copy
// CalligraphyViewModel.swift
import Foundation
import Combine
@MainActor
class CalligraphyViewModel: ObservableObject {
@Published var results: [CalligraphyResult] = []
@Published var usage: CalligraphyUsage?
@Published var isLoading = false
@Published var errorMessage: String?
@Published var showingResultDetail = false
@Published var selectedResult: CalligraphyResult?
@Published var downloadedSvg: SvgData?
@Published var downloadedImage: ImageData?
private let apiService = CalligraphyAPIService.shared
func generateCalligraphy(
text: String,
language: String = "hindi",
fontStyle: String = "calligraphy",
count: Int = 1
) async {
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
errorMessage = "Please enter some text"
return
}
isLoading = true
errorMessage = nil
do {
let data = try await apiService.generateCalligraphy(
text: text,
language: language,
fontStyle: fontStyle,
count: count
)
results = data.results
usage = data.usage
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
func downloadSvg(resultText: String, fontName: String) async {
isLoading = true
errorMessage = nil
do {
let svgData = try await apiService.downloadSvg(
resultText: resultText,
fontName: fontName
)
downloadedSvg = svgData
// Save SVG to Files app
await saveSvgToFiles(svgData: svgData)
} catch {
errorMessage = "Failed to download SVG: \(error.localizedDescription)"
}
isLoading = false
}
func downloadImage(
resultText: String,
fontName: String,
width: Int = 800,
height: Int = 300
) async {
isLoading = true
errorMessage = nil
do {
let imageData = try await apiService.downloadImage(
resultText: resultText,
fontName: fontName,
width: width,
height: height
)
downloadedImage = imageData
// Convert base64 to image and save
await saveImageToFiles(imageData: imageData)
} catch {
errorMessage = "Failed to download image: \(error.localizedDescription)"
}
isLoading = false
}
func clearResults() {
results = []
usage = nil
downloadedSvg = nil
downloadedImage = nil
errorMessage = nil
}
private func saveSvgToFiles(svgData: SvgData) async {
do {
let documentsPath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask)[0]
let fileName = "calligraphy_\(Date().timeIntervalSince1970).svg"
let fileURL = documentsPath.appendingPathComponent(fileName)
try svgData.svgString.write(to: fileURL, atomically: true, encoding: .utf8)
print("SVG saved to: \(fileURL)")
} catch {
errorMessage = "Failed to save SVG file"
}
}
private func saveImageToFiles(imageData: ImageData) async {
do {
// Decode base64 image
let base64String = imageData.imageBase64.replacingOccurrences(of: "data:image/\(imageData.format);base64,", with: "")
guard let data = Data(base64Encoded: base64String) else {
errorMessage = "Failed to decode image data"
return
}
let documentsPath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask)[0]
let fileName = "calligraphy_\(Date().timeIntervalSince1970).\(imageData.format)"
let fileURL = documentsPath.appendingPathComponent(fileName)
try data.write(to: fileURL)
print("Image saved to: \(fileURL)")
} catch {
errorMessage = "Failed to save image file"
}
}
}
// MARK: - Extensions for better error handling
extension CalligraphyViewModel {
func handleError(_ error: Error) {
if let apiError = error as? APIError {
switch apiError {
case .httpError(401):
errorMessage = "Invalid API key. Please check your credentials."
case .httpError(402):
errorMessage = "Insufficient credits. Please upgrade your plan."
case .httpError(429):
errorMessage = "Rate limit exceeded. Please try again later."
default:
errorMessage = apiError.localizedDescription
}
} else {
errorMessage = error.localizedDescription
}
}
}
// MARK: - Input Validation
extension CalligraphyViewModel {
func validateInput(text: String) -> Bool {
let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedText.isEmpty else {
errorMessage = "Please enter some text"
return false
}
guard trimmedText.count >= 2 else {
errorMessage = "Text must be at least 2 characters long"
return false
}
guard trimmedText.count <= 200 else {
errorMessage = "Text must be less than 200 characters"
return false
}
return true
}
}UIKit Implementation (Alternative)
ViewController.swift
swift
Click to copy
// ViewController.swift - UIKit Implementation
import UIKit
class CalligraphyViewController: UIViewController {
// MARK: - IBOutlets
@IBOutlet weak var textView: UITextView!
@IBOutlet weak var languageSegmentedControl: UISegmentedControl!
@IBOutlet weak var fontStyleSegmentedControl: UISegmentedControl!
@IBOutlet weak var countSlider: UISlider!
@IBOutlet weak var countLabel: UILabel!
@IBOutlet weak var generateButton: UIButton!
@IBOutlet weak var resultsCollectionView: UICollectionView!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
@IBOutlet weak var creditsLabel: UILabel!
// MARK: - Properties
private var results: [CalligraphyResult] = []
private let apiService = CalligraphyAPIService.shared
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupCollectionView()
}
// MARK: - Setup Methods
private func setupUI() {
title = "Calligraphy Generator"
textView.text = "नमस्ते दुनिया"
textView.layer.borderColor = UIColor.systemGray4.cgColor
textView.layer.borderWidth = 1
textView.layer.cornerRadius = 8
languageSegmentedControl.setTitle("Hindi", forSegmentAt: 0)
languageSegmentedControl.setTitle("Marathi", forSegmentAt: 1)
languageSegmentedControl.setTitle("Gujarati", forSegmentAt: 2)
languageSegmentedControl.setTitle("English", forSegmentAt: 3)
languageSegmentedControl.selectedSegmentIndex = 0
fontStyleSegmentedControl.setTitle("Calligraphy", forSegmentAt: 0)
fontStyleSegmentedControl.setTitle("Decorative", forSegmentAt: 1)
fontStyleSegmentedControl.setTitle("Publication", forSegmentAt: 2)
fontStyleSegmentedControl.selectedSegmentIndex = 0
countSlider.minimumValue = 1
countSlider.maximumValue = 5
countSlider.value = 3
updateCountLabel()
generateButton.layer.cornerRadius = 8
generateButton.backgroundColor = .systemBlue
activityIndicator.hidesWhenStopped = true
creditsLabel.isHidden = true
}
private func setupCollectionView() {
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: (view.frame.width - 48) / 2, height: 200)
layout.minimumInteritemSpacing = 16
layout.minimumLineSpacing = 16
layout.sectionInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
resultsCollectionView.collectionViewLayout = layout
resultsCollectionView.delegate = self
resultsCollectionView.dataSource = self
resultsCollectionView.register(CalligraphyResultCell.self,
forCellWithReuseIdentifier: "CalligraphyResultCell")
}
private func updateCountLabel() {
countLabel.text = "Count: \(Int(countSlider.value))"
}
// MARK: - IBActions
@IBAction func sliderValueChanged(_ sender: UISlider) {
updateCountLabel()
}
@IBAction func generateButtonTapped(_ sender: UIButton) {
generateCalligraphy()
}
@IBAction func clearButtonTapped(_ sender: UIButton) {
results.removeAll()
resultsCollectionView.reloadData()
creditsLabel.isHidden = true
}
// MARK: - API Methods
private func generateCalligraphy() {
guard !textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
showAlert(title: "Error", message: "Please enter some text")
return
}
setLoading(true)
let language = getSelectedLanguage()
let fontStyle = getSelectedFontStyle()
let count = Int(countSlider.value)
Task {
do {
let data = try await apiService.generateCalligraphy(
text: textView.text,
language: language,
fontStyle: fontStyle,
count: count
)
await MainActor.run {
self.results = data.results
self.resultsCollectionView.reloadData()
self.updateCreditsLabel(usage: data.usage)
self.setLoading(false)
}
} catch {
await MainActor.run {
self.setLoading(false)
self.showAlert(title: "Error", message: error.localizedDescription)
}
}
}
}
private func getSelectedLanguage() -> String {
switch languageSegmentedControl.selectedSegmentIndex {
case 0: return "hindi"
case 1: return "marathi"
case 2: return "gujarati"
case 3: return "english"
default: return "hindi"
}
}
private func getSelectedFontStyle() -> String {
switch fontStyleSegmentedControl.selectedSegmentIndex {
case 0: return "calligraphy"
case 1: return "decorative"
case 2: return "publication"
default: return "calligraphy"
}
}
// MARK: - Helper Methods
private func setLoading(_ loading: Bool) {
if loading {
activityIndicator.startAnimating()
generateButton.isEnabled = false
} else {
activityIndicator.stopAnimating()
generateButton.isEnabled = true
}
}
private func updateCreditsLabel(usage: CalligraphyUsage) {
creditsLabel.text = "Credits remaining: \(usage.remainingCredits)"
creditsLabel.isHidden = false
}
private func showAlert(title: String, message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
private func showResultDetail(_ result: CalligraphyResult) {
let alert = UIAlertController(
title: "Calligraphy Result",
message: "Text: \(result.resultText)\nFont: \(result.fontFamily)\nVariants: \(result.appliedVariants.count)",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Download SVG", style: .default) { _ in
self.downloadSvg(result: result)
})
alert.addAction(UIAlertAction(title: "Save to Photos", style: .default) { _ in
self.saveToPhotos(result: result)
})
alert.addAction(UIAlertAction(title: "Close", style: .cancel))
present(alert, animated: true)
}
private func downloadSvg(result: CalligraphyResult) {
setLoading(true)
Task {
do {
let svgData = try await apiService.downloadSvg(
resultText: result.resultText,
fontName: result.fontFamily
)
await MainActor.run {
self.setLoading(false)
self.saveSvgToFiles(svgData: svgData)
}
} catch {
await MainActor.run {
self.setLoading(false)
self.showAlert(title: "Error", message: "Failed to download SVG")
}
}
}
}
private func saveToPhotos(result: CalligraphyResult) {
guard let urlString = result.cdnUrl, let url = URL(string: urlString) else {
showAlert(title: "Error", message: "No image URL available")
return
}
Task {
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let image = UIImage(data: data) {
await MainActor.run {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
self.showAlert(title: "Success", message: "Image saved to Photos")
}
}
} catch {
await MainActor.run {
self.showAlert(title: "Error", message: "Failed to save image")
}
}
}
}
private func saveSvgToFiles(svgData: SvgData) {
do {
let documentsPath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask)[0]
let fileName = "calligraphy_\(Date().timeIntervalSince1970).svg"
let fileURL = documentsPath.appendingPathComponent(fileName)
try svgData.svgString.write(to: fileURL, atomically: true, encoding: .utf8)
showAlert(title: "Success", message: "SVG saved to Files app")
} catch {
showAlert(title: "Error", message: "Failed to save SVG file")
}
}
}
// MARK: - Collection View Data Source & Delegate
extension CalligraphyViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return results.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CalligraphyResultCell", for: indexPath) as! CalligraphyResultCell
cell.configure(with: results[indexPath.item])
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
showResultDetail(results[indexPath.item])
}
}
// MARK: - Custom Collection View Cell
class CalligraphyResultCell: UICollectionViewCell {
private let imageView = UIImageView()
private let textLabel = UILabel()
private let fontLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
backgroundColor = .systemBackground
layer.cornerRadius = 8
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 0, height: 2)
layer.shadowRadius = 4
layer.shadowOpacity = 0.1
imageView.contentMode = .scaleAspectFit
imageView.backgroundColor = .systemGray6
imageView.layer.cornerRadius = 4
textLabel.font = .systemFont(ofSize: 12, weight: .medium)
textLabel.numberOfLines = 2
fontLabel.font = .systemFont(ofSize: 10)
fontLabel.textColor = .systemGray
[imageView, textLabel, fontLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
imageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
imageView.heightAnchor.constraint(equalToConstant: 120),
textLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 8),
textLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
textLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
fontLabel.topAnchor.constraint(equalTo: textLabel.bottomAnchor, constant: 4),
fontLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
fontLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
fontLabel.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8)
])
}
func configure(with result: CalligraphyResult) {
textLabel.text = result.resultText
fontLabel.text = result.fontFamily
if let urlString = result.cdnUrl, let url = URL(string: urlString) {
Task {
do {
let (data, _) = try await URLSession.shared.data(from: url)
let image = UIImage(data: data)
await MainActor.run {
self.imageView.image = image
}
} catch {
await MainActor.run {
self.imageView.image = nil
}
}
}
}
}
}What You'll Build
Native iOS App
SwiftUI and UIKit implementations
MVVM Pattern
Clean architecture with ObservableObject
Async/Await Support
Modern Swift concurrency
File Management
Save to Photos and Files app
Requirements
iOS 13.0+ (SwiftUI) / iOS 11.0+ (UIKit)
Swift 5.0+
Xcode 12+
API Key from Calligraphy API
Other Integrations