Swift iOS Development with SwiftUI and UIKit

Comprehensive guide for building iOS applications with Swift, SwiftUI, and UIKit best practices

# Swift iOS Development with SwiftUI and UIKit

## 1. Project Setup and Architecture

### Modern iOS Project Structure
```
MyApp/
├── MyApp/
│   ├── App/
│   │   ├── MyAppApp.swift
│   │   ├── ContentView.swift
│   │   └── Info.plist
│   ├── Features/
│   │   ├── Authentication/
│   │   │   ├── Views/
│   │   │   ├── ViewModels/
│   │   │   └── Services/
│   │   ├── Profile/
│   │   └── Settings/
│   ├── Core/
│   │   ├── Models/
│   │   ├── Services/
│   │   ├── Network/
│   │   ├── Storage/
│   │   └── Extensions/
│   ├── Shared/
│   │   ├── Components/
│   │   ├── Modifiers/
│   │   ├── Utils/
│   │   └── Constants/
│   └── Resources/
│       ├── Assets.xcassets
│       ├── Colors.xcassets
│       └── Localizable.strings
├── MyAppTests/
├── MyAppUITests/
└── Packages/ (if using local SPM packages)
```

### SwiftUI App Structure with MVVM
```swift
// MyAppApp.swift
import SwiftUI

@main
struct MyAppApp: App {
    @StateObject private var appState = AppState()
    @StateObject private var authenticationManager = AuthenticationManager()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(appState)
                .environmentObject(authenticationManager)
                .onAppear {
                    setupApp()
                }
        }
    }

    private func setupApp() {
        // Configure app-wide settings
        configureNetworking()
        configureAnalytics()
        authenticationManager.checkAuthenticationStatus()
    }

    private func configureNetworking() {
        NetworkManager.shared.configure(
            baseURL: Configuration.apiBaseURL,
            timeout: 30.0
        )
    }

    private func configureAnalytics() {
        #if !DEBUG
        AnalyticsManager.shared.configure()
        #endif
    }
}
```

## 2. SwiftUI Views and Components

### Reusable SwiftUI Components
```swift
// Custom Button Component
struct PrimaryButton: View {
    let title: String
    let action: () -> Void
    let isLoading: Bool
    let isDisabled: Bool

    init(
        _ title: String,
        isLoading: Bool = false,
        isDisabled: Bool = false,
        action: @escaping () -> Void
    ) {
        self.title = title
        self.isLoading = isLoading
        self.isDisabled = isDisabled
        self.action = action
    }

    var body: some View {
        Button(action: action) {
            HStack {
                if isLoading {
                    ProgressView()
                        .progressViewStyle(CircularProgressViewStyle(tint: .white))
                        .scaleEffect(0.8)
                } else {
                    Text(title)
                        .font(.headline)
                        .fontWeight(.semibold)
                }
            }
            .frame(maxWidth: .infinity)
            .frame(height: 50)
            .foregroundColor(.white)
            .background(
                isDisabled ? Color.gray : Color.accentColor
            )
            .cornerRadius(12)
            .disabled(isDisabled || isLoading)
        }
        .animation(.easeInOut(duration: 0.2), value: isLoading)
    }
}

// Custom Text Field Component
struct CustomTextField: View {
    let title: String
    @Binding var text: String
    let placeholder: String
    let isSecure: Bool
    let keyboardType: UIKeyboardType
    let validation: ((String) -> String?)?

    @State private var isEditing = false
    @State private var validationMessage: String?

    init(
        _ title: String,
        text: Binding<String>,
        placeholder: String = "",
        isSecure: Bool = false,
        keyboardType: UIKeyboardType = .default,
        validation: ((String) -> String?)? = nil
    ) {
        self.title = title
        self._text = text
        self.placeholder = placeholder
        self.isSecure = isSecure
        self.keyboardType = keyboardType
        self.validation = validation
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(title)
                .font(.subheadline)
                .fontWeight(.medium)
                .foregroundColor(.primary)

            Group {
                if isSecure {
                    SecureField(placeholder, text: $text)
                } else {
                    TextField(placeholder, text: $text)
                        .keyboardType(keyboardType)
                }
            }
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .onEditingChanged { editing in
                isEditing = editing
                if !editing {
                    validateInput()
                }
            }

            if let validationMessage = validationMessage {
                Text(validationMessage)
                    .font(.caption)
                    .foregroundColor(.red)
                    .transition(.opacity)
            }
        }
        .animation(.easeInOut(duration: 0.2), value: validationMessage)
    }

    private func validateInput() {
        validationMessage = validation?(text)
    }
}

// Custom Card Component
struct Card<Content: View>: View {
    let content: Content
    let padding: EdgeInsets
    let cornerRadius: CGFloat
    let shadow: Bool

    init(
        padding: EdgeInsets = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16),
        cornerRadius: CGFloat = 12,
        shadow: Bool = true,
        @ViewBuilder content: () -> Content
    ) {
        self.content = content()
        self.padding = padding
        self.cornerRadius = cornerRadius
        self.shadow = shadow
    }

    var body: some View {
        content
            .padding(padding)
            .background(Color(.systemBackground))
            .cornerRadius(cornerRadius)
            .conditionalModifier(shadow) { view in
                view.shadow(
                    color: Color.black.opacity(0.1),
                    radius: 8,
                    x: 0,
                    y: 2
                )
            }
    }
}
```

### Advanced SwiftUI Patterns
```swift
// View Modifier for Conditional Modifications
extension View {
    @ViewBuilder
    func conditionalModifier<Content: View>(
        _ condition: Bool,
        transform: (Self) -> Content
    ) -> some View {
        if condition {
            transform(self)
        } else {
            self
        }
    }
}

// Custom Environment Key
private struct UserPreferencesKey: EnvironmentKey {
    static let defaultValue = UserPreferences()
}

extension EnvironmentValues {
    var userPreferences: UserPreferences {
        get { self[UserPreferencesKey.self] }
        set { self[UserPreferencesKey.self] = newValue }
    }
}

// Preference Key for Dynamic Content
struct SizePreferenceKey: PreferenceKey {
    static var defaultValue: CGSize = .zero

    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        let next = nextValue()
        value = CGSize(
            width: max(value.width, next.width),
            height: max(value.height, next.height)
        )
    }
}

extension View {
    func measureSize(_ perform: @escaping (CGSize) -> Void) -> some View {
        background(
            GeometryReader { geometry in
                Color.clear
                    .preference(key: SizePreferenceKey.self, value: geometry.size)
            }
        )
        .onPreferenceChange(SizePreferenceKey.self, perform: perform)
    }
}
```

## 3. MVVM Architecture with Combine

### Observable ViewModel Base Class
```swift
import Foundation
import Combine

// Base ViewModel Protocol
protocol ViewModelProtocol: ObservableObject {
    var isLoading: Bool { get set }
    var errorMessage: String? { get set }

    func handleError(_ error: Error)
}

// Base ViewModel Implementation
class BaseViewModel: ViewModelProtocol {
    @Published var isLoading = false
    @Published var errorMessage: String?

    protected var cancellables = Set<AnyCancellable>()

    func handleError(_ error: Error) {
        DispatchQueue.main.async {
            self.isLoading = false

            if let appError = error as? AppError {
                self.errorMessage = appError.localizedDescription
            } else {
                self.errorMessage = "An unexpected error occurred"
            }
        }
    }

    func clearError() {
        errorMessage = nil
    }
}

// User Profile ViewModel Example
class UserProfileViewModel: BaseViewModel {
    @Published var user: User?
    @Published var posts: [Post] = []

    private let userService: UserServiceProtocol
    private let postService: PostServiceProtocol

    init(
        userService: UserServiceProtocol = UserService(),
        postService: PostServiceProtocol = PostService()
    ) {
        self.userService = userService
        self.postService = postService
        super.init()
    }

    func loadUserProfile(userId: String) {
        isLoading = true
        clearError()

        Publishers.CombineLatest(
            userService.fetchUser(id: userId),
            postService.fetchUserPosts(userId: userId)
        )
        .receive(on: DispatchQueue.main)
        .sink(
            receiveCompletion: { [weak self] completion in
                self?.isLoading = false
                if case .failure(let error) = completion {
                    self?.handleError(error)
                }
            },
            receiveValue: { [weak self] user, posts in
                self?.user = user
                self?.posts = posts
            }
        )
        .store(in: &cancellables)
    }

    func refreshProfile() {
        guard let userId = user?.id else { return }
        loadUserProfile(userId: userId)
    }
}
```

### Service Layer with Combine
```swift
import Foundation
import Combine

// Network Service Protocol
protocol NetworkServiceProtocol {
    func request<T: Codable>(
        endpoint: APIEndpoint,
        responseType: T.Type
    ) -> AnyPublisher<T, AppError>
}

// Network Service Implementation
class NetworkService: NetworkServiceProtocol {
    static let shared = NetworkService()

    private let session: URLSession
    private let baseURL: URL

    init(session: URLSession = .shared, baseURL: URL = Configuration.apiBaseURL) {
        self.session = session
        self.baseURL = baseURL
    }

    func request<T: Codable>(
        endpoint: APIEndpoint,
        responseType: T.Type
    ) -> AnyPublisher<T, AppError> {
        guard let url = buildURL(for: endpoint) else {
            return Fail(error: AppError.invalidURL)
                .eraseToAnyPublisher()
        }

        let request = buildRequest(url: url, endpoint: endpoint)

        return session.dataTaskPublisher(for: request)
            .tryMap { data, response in
                try self.validateResponse(data: data, response: response)
                return data
            }
            .decode(type: T.self, decoder: JSONDecoder.apiDecoder)
            .mapError { error in
                if let appError = error as? AppError {
                    return appError
                } else if error is DecodingError {
                    return AppError.decodingError
                } else {
                    return AppError.networkError(error.localizedDescription)
                }
            }
            .eraseToAnyPublisher()
    }

    private func buildURL(for endpoint: APIEndpoint) -> URL? {
        var components = URLComponents(url: baseURL.appendingPathComponent(endpoint.path), resolvingAgainstBaseURL: false)
        components?.queryItems = endpoint.queryItems
        return components?.url
    }

    private func buildRequest(url: URL, endpoint: APIEndpoint) -> URLRequest {
        var request = URLRequest(url: url)
        request.httpMethod = endpoint.method.rawValue
        request.allHTTPHeaderFields = endpoint.headers
        request.httpBody = endpoint.body
        return request
    }

    private func validateResponse(data: Data, response: URLResponse) throws {
        guard let httpResponse = response as? HTTPURLResponse else {
            throw AppError.invalidResponse
        }

        guard 200...299 ~= httpResponse.statusCode else {
            throw AppError.httpError(httpResponse.statusCode)
        }
    }
}

// User Service Implementation
protocol UserServiceProtocol {
    func fetchUser(id: String) -> AnyPublisher<User, AppError>
    func updateUser(_ user: User) -> AnyPublisher<User, AppError>
}

class UserService: UserServiceProtocol {
    private let networkService: NetworkServiceProtocol

    init(networkService: NetworkServiceProtocol = NetworkService.shared) {
        self.networkService = networkService
    }

    func fetchUser(id: String) -> AnyPublisher<User, AppError> {
        let endpoint = APIEndpoint.user(id: id)
        return networkService.request(endpoint: endpoint, responseType: User.self)
    }

    func updateUser(_ user: User) -> AnyPublisher<User, AppError> {
        let endpoint = APIEndpoint.updateUser(user)
        return networkService.request(endpoint: endpoint, responseType: User.self)
    }
}
```

## 4. Core Data Integration

### Core Data Stack Setup
```swift
import CoreData
import Combine

class CoreDataStack: ObservableObject {
    static let shared = CoreDataStack()

    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "DataModel")

        container.loadPersistentStores { _, error in
            if let error = error {
                fatalError("Core Data error: \(error.localizedDescription)")
            }
        }

        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

        return container
    }()

    var viewContext: NSManagedObjectContext {
        persistentContainer.viewContext
    }

    func newBackgroundContext() -> NSManagedObjectContext {
        persistentContainer.newBackgroundContext()
    }

    func save() {
        let context = viewContext

        if context.hasChanges {
            do {
                try context.save()
            } catch {
                print("Save error: \(error)")
            }
        }
    }

    func saveBackground(_ context: NSManagedObjectContext) {
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                print("Background save error: \(error)")
            }
        }
    }
}

// Repository Pattern for Core Data
protocol UserRepositoryProtocol {
    func fetchUsers() -> AnyPublisher<[UserEntity], Error>
    func save(user: User) -> AnyPublisher<UserEntity, Error>
    func delete(userEntity: UserEntity) -> AnyPublisher<Void, Error>
}

class UserRepository: UserRepositoryProtocol {
    private let coreDataStack: CoreDataStack

    init(coreDataStack: CoreDataStack = .shared) {
        self.coreDataStack = coreDataStack
    }

    func fetchUsers() -> AnyPublisher<[UserEntity], Error> {
        Future { promise in
            let context = self.coreDataStack.viewContext
            let request: NSFetchRequest<UserEntity> = UserEntity.fetchRequest()
            request.sortDescriptors = [NSSortDescriptor(keyPath: \UserEntity.name, ascending: true)]

            do {
                let users = try context.fetch(request)
                promise(.success(users))
            } catch {
                promise(.failure(error))
            }
        }
        .eraseToAnyPublisher()
    }

    func save(user: User) -> AnyPublisher<UserEntity, Error> {
        Future { promise in
            let context = self.coreDataStack.newBackgroundContext()

            context.perform {
                let userEntity = UserEntity(context: context)
                userEntity.id = user.id
                userEntity.name = user.name
                userEntity.email = user.email
                userEntity.createdAt = Date()

                do {
                    try context.save()
                    promise(.success(userEntity))
                } catch {
                    promise(.failure(error))
                }
            }
        }
        .eraseToAnyPublisher()
    }

    func delete(userEntity: UserEntity) -> AnyPublisher<Void, Error> {
        Future { promise in
            let context = self.coreDataStack.viewContext
            context.delete(userEntity)

            do {
                try context.save()
                promise(.success(()))
            } catch {
                promise(.failure(error))
            }
        }
        .eraseToAnyPublisher()
    }
}
```

## 5. UIKit Integration with SwiftUI

### UIViewRepresentable for Custom UIKit Components
```swift
import SwiftUI
import UIKit

// Custom UITextField Wrapper
struct UITextFieldWrapper: UIViewRepresentable {
    @Binding var text: String
    let placeholder: String
    let isFirstResponder: Bool
    let onCommit: () -> Void

    func makeUIView(context: Context) -> UITextField {
        let textField = UITextField()
        textField.placeholder = placeholder
        textField.delegate = context.coordinator
        textField.borderStyle = .roundedRect
        textField.font = UIFont.systemFont(ofSize: 16)

        // Add toolbar with done button
        let toolbar = UIToolbar()
        toolbar.sizeToFit()
        let doneButton = UIBarButtonItem(
            barButtonSystemItem: .done,
            target: context.coordinator,
            action: #selector(Coordinator.donePressed)
        )
        toolbar.setItems([UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), doneButton], animated: false)
        textField.inputAccessoryView = toolbar

        return textField
    }

    func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.text = text

        if isFirstResponder && !uiView.isFirstResponder {
            uiView.becomeFirstResponder()
        } else if !isFirstResponder && uiView.isFirstResponder {
            uiView.resignFirstResponder()
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, UITextFieldDelegate {
        let parent: UITextFieldWrapper

        init(_ parent: UITextFieldWrapper) {
            self.parent = parent
        }

        func textFieldDidChangeSelection(_ textField: UITextField) {
            parent.text = textField.text ?? ""
        }

        func textFieldDidEndEditing(_ textField: UITextField) {
            parent.onCommit()
        }

        @objc func donePressed() {
            UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
        }
    }
}

// UIViewControllerRepresentable for Complex Screens
struct ImagePickerView: UIViewControllerRepresentable {
    @Binding var selectedImage: UIImage?
    @Environment(.presentationMode) var presentationMode

    let sourceType: UIImagePickerController.SourceType

    func makeUIViewController(context: Context) -> UIImagePickerController {
        let picker = UIImagePickerController()
        picker.sourceType = sourceType
        picker.delegate = context.coordinator
        return picker
    }

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        let parent: ImagePickerView

        init(_ parent: ImagePickerView) {
            self.parent = parent
        }

        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            if let image = info[.originalImage] as? UIImage {
                parent.selectedImage = image
            }
            parent.presentationMode.wrappedValue.dismiss()
        }

        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            parent.presentationMode.wrappedValue.dismiss()
        }
    }
}
```

## 6. Testing Strategies

### Unit Testing ViewModels
```swift
import XCTest
import Combine
@testable import MyApp

class UserProfileViewModelTests: XCTestCase {
    var viewModel: UserProfileViewModel!
    var mockUserService: MockUserService!
    var mockPostService: MockPostService!
    var cancellables: Set<AnyCancellable>!

    override func setUp() {
        super.setUp()
        mockUserService = MockUserService()
        mockPostService = MockPostService()
        viewModel = UserProfileViewModel(
            userService: mockUserService,
            postService: mockPostService
        )
        cancellables = Set<AnyCancellable>()
    }

    override func tearDown() {
        viewModel = nil
        mockUserService = nil
        mockPostService = nil
        cancellables = nil
        super.tearDown()
    }

    func testLoadUserProfileSuccess() {
        // Given
        let expectedUser = User.mock()
        let expectedPosts = [Post.mock(), Post.mock()]

        mockUserService.fetchUserResult = .success(expectedUser)
        mockPostService.fetchUserPostsResult = .success(expectedPosts)

        let expectation = XCTestExpectation(description: "Load user profile"
	categories: ["development", "mobile"],)

        // When
        viewModel.$isLoading
            .dropFirst() // Skip initial false value
            .sink { isLoading in
                if !isLoading {
                    expectation.fulfill()
                }
            }
            .store(in: &cancellables)

        viewModel.loadUserProfile(userId: "123")

        // Then
        wait(for: [expectation], timeout: 1.0)

        XCTAssertEqual(viewModel.user, expectedUser)
        XCTAssertEqual(viewModel.posts, expectedPosts)
        XCTAssertFalse(viewModel.isLoading)
        XCTAssertNil(viewModel.errorMessage)
    }

    func testLoadUserProfileFailure() {
        // Given
        let expectedError = AppError.networkError("Network failure")
        mockUserService.fetchUserResult = .failure(expectedError)

        let expectation = XCTestExpectation(description: "Handle error"
	categories: ["development", "mobile"],)

        // When
        viewModel.$errorMessage
            .compactMap { $0 }
            .sink { _ in
                expectation.fulfill()
            }
            .store(in: &cancellables)

        viewModel.loadUserProfile(userId: "123")

        // Then
        wait(for: [expectation], timeout: 1.0)

        XCTAssertNil(viewModel.user)
        XCTAssertTrue(viewModel.posts.isEmpty)
        XCTAssertFalse(viewModel.isLoading)
        XCTAssertNotNil(viewModel.errorMessage)
    }
}

// Mock Services for Testing
class MockUserService: UserServiceProtocol {
    var fetchUserResult: Result<User, AppError>!

    func fetchUser(id: String) -> AnyPublisher<User, AppError> {
        Future { promise in
            promise(self.fetchUserResult)
        }
        .eraseToAnyPublisher()
    }

    func updateUser(_ user: User) -> AnyPublisher<User, AppError> {
        Just(user)
            .setFailureType(to: AppError.self)
            .eraseToAnyPublisher()
    }
}
```

### SwiftUI View Testing
```swift
import XCTest
import SwiftUI
@testable import MyApp

class ContentViewTests: XCTestCase {

    func testLoginButtonTapped() {
        // Given
        let viewModel = AuthenticationViewModel()
        let view = ContentView()
            .environmentObject(viewModel)

        // When
        let hostingController = UIHostingController(rootView: view)
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = hostingController
        window.makeKeyAndVisible()

        // Find and tap login button
        let loginButton = hostingController.view.findButton(withText: "Login")
        loginButton?.sendActions(for: .touchUpInside)

        // Then
        XCTAssertTrue(viewModel.isShowingLogin)
    }
}

extension UIView {
    func findButton(withText text: String) -> UIButton? {
        if let button = self as? UIButton, button.titleLabel?.text == text {
            return button
        }

        for subview in subviews {
            if let found = subview.findButton(withText: text) {
                return found
            }
        }

        return nil
    }
}
```

## 7. Performance Optimization

### Memory Management and Performance
```swift
// Lazy Loading for Expensive Operations
struct ExpensiveView: View {
    @State private var expensiveData: [DataModel] = []
    @State private var isLoaded = false

    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(expensiveData) { item in
                    ItemView(item: item)
                        .onAppear {
                            // Load more data when needed
                            loadMoreDataIfNeeded(item: item)
                        }
                }
            }
        }
        .onAppear {
            if !isLoaded {
                loadInitialData()
                isLoaded = true
            }
        }
    }

    private func loadInitialData() {
        // Load initial batch of data
    }

    private func loadMoreDataIfNeeded(item: DataModel) {
        // Implement pagination logic
    }
}

// Image Caching for Better Performance
class ImageCache {
    static let shared = ImageCache()
    private let cache = NSCache<NSString, UIImage>()

    private init() {
        cache.countLimit = 100
        cache.totalCostLimit = 50 * 1024 * 1024 // 50MB
    }

    func image(for url: String) -> UIImage? {
        return cache.object(forKey: NSString(string: url))
    }

    func setImage(_ image: UIImage, for url: String) {
        cache.setObject(image, forKey: NSString(string: url))
    }
}

// Async Image Loading with SwiftUI
struct AsyncImageView: View {
    let url: URL
    @State private var image: UIImage?
    @State private var isLoading = true

    var body: some View {
        Group {
            if let image = image {
                Image(uiImage: image)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            } else if isLoading {
                ProgressView()
            } else {
                Image(systemName: "photo")
                    .foregroundColor(.gray)
            }
        }
        .onAppear {
            loadImage()
        }
    }

    private func loadImage() {
        // Check cache first
        if let cachedImage = ImageCache.shared.image(for: url.absoluteString) {
            self.image = cachedImage
            self.isLoading = false
            return
        }

        // Load from network
        URLSession.shared.dataTask(with: url) { data, _, _ in
            guard let data = data, let loadedImage = UIImage(data: data) else {
                DispatchQueue.main.async {
                    self.isLoading = false
                }
                return
            }

            // Cache the image
            ImageCache.shared.setImage(loadedImage, for: url.absoluteString)

            DispatchQueue.main.async {
                self.image = loadedImage
                self.isLoading = false
            }
        }.resume()
    }
}
```

## Checklist for Swift iOS Development

- [ ] Set up proper MVVM architecture with SwiftUI
- [ ] Implement reusable SwiftUI components and modifiers
- [ ] Create comprehensive ViewModels with Combine
- [ ] Set up Core Data stack with repository pattern
- [ ] Integrate UIKit components when needed with UIViewRepresentable
- [ ] Implement proper error handling and user feedback
- [ ] Add comprehensive unit and UI tests
- [ ] Optimize performance with lazy loading and caching
- [ ] Follow iOS design guidelines and accessibility standards
- [ ] Implement proper navigation and state management
- [ ] Add logging and analytics integration
- [ ] Configure proper networking with error handling
- [ ] Implement offline capabilities and data persistence
- [ ] Add proper app lifecycle management
Swift iOS Development with SwiftUI and UIKit - Cursor IDE AI Rule