Skip to main content
Glama
ios-swift-development-2025.md14.8 kB
# iOS Swift Development 2025 **Updated**: 2025-11-23 | **Stack**: Swift 5.9, SwiftUI, Combine, Xcode 15 --- ## SwiftUI Basics ```swift import SwiftUI // Simple View struct ContentView: View { @State private var count = 0 var body: some View { VStack(spacing: 20) { Text("Count: \(count)") .font(.largeTitle) Button("Increment") { count += 1 } .buttonStyle(.borderedProminent) } .padding() } } --- // List with Navigation struct TodoListView: View { @State private var todos = ["Buy milk", "Walk dog", "Code"] @State private var newTodo = "" var body: some View { NavigationStack { List { ForEach(todos, id: \.self) { todo in Text(todo) } .onDelete { indexSet in todos.remove(atOffsets: indexSet) } } .navigationTitle("Todos") .toolbar { ToolbarItem(placement: .primaryAction) { Button("Add") { if !newTodo.isEmpty { todos.append(newTodo) newTodo = "" } } } } .searchable(text: $newTodo, prompt: "Add new todo") } } } --- // MVVM Architecture class TodoViewModel: ObservableObject { @Published var todos: [Todo] = [] @Published var isLoading = false func fetchTodos() async { isLoading = true defer { isLoading = false } do { let url = URL(string: "https://api.example.com/todos")! let (data, _) = try await URLSession.shared.data(from: url) todos = try JSONDecoder().decode([Todo].self, from: data) } catch { print("Error: \(error)") } } func addTodo(_ text: String) { let todo = Todo(id: UUID(), text: text, completed: false) todos.append(todo) } func toggleTodo(_ todo: Todo) { if let index = todos.firstIndex(where: { $0.id == todo.id }) { todos[index].completed.toggle() } } } struct Todo: Identifiable, Codable { let id: UUID var text: String var completed: Bool } struct TodoView: View { @StateObject private var viewModel = TodoViewModel() var body: some View { List(viewModel.todos) { todo in HStack { Image(systemName: todo.completed ? "checkmark.circle.fill" : "circle") .foregroundColor(todo.completed ? .green : .gray) Text(todo.text) .strikethrough(todo.completed) } .onTapGesture { viewModel.toggleTodo(todo) } } .task { await viewModel.fetchTodos() } .overlay { if viewModel.isLoading { ProgressView() } } } } ``` --- ## Networking ```swift // Modern async/await API calls actor NetworkManager { static let shared = NetworkManager() private init() {} func fetch<T: Decodable>(_ url: URL) async throws -> T { let (data, response) = try await URLSession.shared.data(from: url) guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { throw NetworkError.invalidResponse } return try JSONDecoder().decode(T.self, from: data) } func post<T: Encodable, R: Decodable>(_ url: URL, body: T) async throws -> R { var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONEncoder().encode(body) let (data, _) = try await URLSession.shared.data(for: request) return try JSONDecoder().decode(R.self, from: data) } } enum NetworkError: Error { case invalidURL case invalidResponse case decodingError } // Usage Task { do { let todos: [Todo] = try await NetworkManager.shared.fetch( URL(string: "https://api.example.com/todos")! ) print(todos) } catch { print("Error: \(error)") } } --- // Image loading with caching @MainActor class ImageLoader: ObservableObject { @Published var image: UIImage? private static let cache = NSCache<NSURL, UIImage>() func load(from url: URL) async { // Check cache if let cached = Self.cache.object(forKey: url as NSURL) { image = cached return } // Download do { let (data, _) = try await URLSession.shared.data(from: url) if let downloadedImage = UIImage(data: data) { Self.cache.setObject(downloadedImage, forKey: url as NSURL) image = downloadedImage } } catch { print("Failed to load image: \(error)") } } } struct AsyncImageView: View { let url: URL @StateObject private var loader = ImageLoader() var body: some View { Group { if let image = loader.image { Image(uiImage: image) .resizable() .scaledToFit() } else { ProgressView() } } .task { await loader.load(from: url) } } } ``` --- ## Data Persistence ```swift // UserDefaults (Simple data) extension UserDefaults { enum Keys { static let username = "username" static let isDarkMode = "isDarkMode" } } // Save UserDefaults.standard.set("John", forKey: UserDefaults.Keys.username) UserDefaults.standard.set(true, forKey: UserDefaults.Keys.isDarkMode) // Read let username = UserDefaults.standard.string(forKey: UserDefaults.Keys.username) let isDarkMode = UserDefaults.standard.bool(forKey: UserDefaults.Keys.isDarkMode) --- // SwiftData (iOS 17+, replaces CoreData) import SwiftData @Model class Task { @Attribute(.unique) var id: UUID var title: String var completed: Bool var createdAt: Date init(title: String) { self.id = UUID() self.title = title self.completed = false self.createdAt = Date() } } @main struct TodoApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Task.self) } } struct ContentView: View { @Environment(\.modelContext) private var context @Query private var tasks: [Task] var body: some View { List { ForEach(tasks) { task in HStack { Text(task.title) Spacer() if task.completed { Image(systemName: "checkmark") } } .onTapGesture { task.completed.toggle() } } .onDelete { indexSet in for index in indexSet { context.delete(tasks[index]) } } } .toolbar { Button("Add") { let task = Task(title: "New Task") context.insert(task) } } } } --- // Keychain (Secure storage for passwords, tokens) import Security class KeychainManager { static func save(key: String, data: Data) -> Bool { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecValueData as String: data ] SecItemDelete(query as CFDictionary) // Delete old let status = SecItemAdd(query as CFDictionary, nil) return status == errSecSuccess } static func load(key: String) -> Data? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecReturnData as String: true ] var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) return status == errSecSuccess ? result as? Data : nil } } // Usage let token = "secret_token".data(using: .utf8)! KeychainManager.save(key: "authToken", data: token) if let data = KeychainManager.load(key: "authToken"), let token = String(data: data, encoding: .utf8) { print("Token: \(token)") } ``` --- ## Push Notifications ```swift import UserNotifications class NotificationManager { static let shared = NotificationManager() func requestAuthorization() async -> Bool { let center = UNUserNotificationCenter.current() do { let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge]) return granted } catch { print("Error: \(error)") return false } } func scheduleNotification(title: String, body: String, after seconds: TimeInterval) { let content = UNMutableNotificationContent() content.title = title content.body = body content.sound = .default let trigger = UNTimeIntervalNotificationTrigger(timeInterval: seconds, repeats: false) let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) } } // Usage Task { let granted = await NotificationManager.shared.requestAuthorization() if granted { NotificationManager.shared.scheduleNotification( title: "Reminder", body: "Time to take a break!", after: 60 // 1 minute ) } } --- // AppDelegate for push notifications class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { UNUserNotificationCenter.current().delegate = self application.registerForRemoteNotifications() return true } func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() print("Device Token: \(token)") // Send to your server } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { return [.banner, .sound] } } @main struct MyApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } ``` --- ## Testing ```swift import XCTest @testable import MyApp final class TodoViewModelTests: XCTestCase { var viewModel: TodoViewModel! override func setUp() { super.setUp() viewModel = TodoViewModel() } override func tearDown() { viewModel = nil super.tearDown() } func testAddTodo() { // Given let initialCount = viewModel.todos.count // When viewModel.addTodo("Test task") // Then XCTAssertEqual(viewModel.todos.count, initialCount + 1) XCTAssertEqual(viewModel.todos.last?.text, "Test task") XCTAssertFalse(viewModel.todos.last?.completed ?? true) } func testToggleTodo() { // Given viewModel.addTodo("Test task") let todo = viewModel.todos[0] let initialState = todo.completed // When viewModel.toggleTodo(todo) // Then XCTAssertEqual(viewModel.todos[0].completed, !initialState) } func testFetchTodos() async { // Given XCTAssertTrue(viewModel.todos.isEmpty) // When await viewModel.fetchTodos() // Then XCTAssertFalse(viewModel.todos.isEmpty) } } // Run tests: Cmd+U ``` --- ## App Store Submission ```markdown PREPARATION: XCODE: 1. Set version & build number (1.0.0, build 1) 2. Select "Any iOS Device" scheme 3. Product → Archive 4. Organizer → Distribute App → App Store Connect APP STORE CONNECT: 1. Create app listing 2. App Name, Subtitle, Description 3. Keywords (100 characters, comma-separated) 4. Screenshots (6.5", 5.5" required) 5. App Icon (1024×1024px) 6. Privacy Policy URL 7. Age Rating 8. Pricing ($0.99, $2.99, etc.) REVIEW PROCESS: - Submit for Review - Apple reviews (1-3 days typically) - Feedback if rejected (address issues, resubmit) - Approved → Release (manual or automatic) --- REQUIREMENTS: TECHNICAL: - No crashes (test thoroughly!) - Works on all supported devices - Complies with guidelines (no private APIs) CONTENT: - Accurate description - Appropriate age rating - No spam, scam, or misleading LEGAL: - Privacy policy (if collecting data) - Terms of service - GDPR, COPPA compliance --- APP STORE OPTIMIZATION (ASO): TITLE: - Include main keyword - "Task Manager - Todo List & Planner" SUBTITLE: - 30 characters - Additional keywords - "Organize your life, boost productivity" KEYWORDS: - 100 characters - No spaces after commas - "todo,task,planner,productivity,gtd" ICON: - Simple, recognizable - No text (iOS adds app name) - Test on home screen (looks good small?) SCREENSHOTS: - Show best features (first 3 most important) - Use mockups (Device Frame Generator) - Add text overlays (explain features) RATINGS & REVIEWS: - Prompt after positive experience (not at launch!) - Respond to negative reviews (shows you care) - SKStoreReviewController (max 3x/year) ``` --- ## Key Takeaways 1. **SwiftUI** - Declarative, reactive, future of iOS dev 2. **async/await** - Modern concurrency (simpler than closures) 3. **MVVM** - Separate logic from UI 4. **Testing** - Write tests (save time debugging) 5. **App Store** - Polish, test, optimize listing --- ## References - Swift Documentation - Apple Human Interface Guidelines - "SwiftUI by Example" - Paul Hudson - WWDC Videos **Related**: `swiftui-advanced.md`, `combine-reactive.md`, `app-store-optimization.md`

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/seanshin0214/persona-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server