Swift & SwiftUI iOS Development Guide
Introduction
Swift and SwiftUI are Apple's modern tools for building iOS, iPadOS, macOS, watchOS, and tvOS applications. SwiftUI's declarative syntax makes it easy to build beautiful, responsive interfaces with less code.
1. Swift Fundamentals
// Variables and Constants
var mutableValue = 10
let constantValue = 20
// Optionals
var name: String? = "John"
if let unwrappedName = name {
print("Hello, \(unwrappedName)")
}
// Nil coalescing
let displayName = name ?? "Guest"
// Guard statements
func greet(name: String?) {
guard let name = name else { return }
print("Hello, \(name)")
}
// Structures
struct User {
var id: Int
var name: String
var email: String
func isValid() -> Bool {
return !email.isEmpty
}
}
// Classes
class ViewModel: ObservableObject {
@Published var count = 0
func increment() {
count += 1
}
}
// Enums with associated values
enum Result<T, E: Error> {
case success(T)
case failure(E)
}
// Closures
let numbers = [1, 2, 3, 4, 5]
let doubled = numbers.map { $0 * 2 }
let filtered = numbers.filter { $0 > 2 }
// Async/await
func fetchData() async throws -> String {
let url = URL(string: "https://api.example.com/data")!
let (data, _) = try await URLSession.shared.data(from: url)
return String(data: data, encoding: .utf8) ?? ""
}
2. SwiftUI Basics
import SwiftUI
struct ContentView: View {
@State private var count = 0
@State private var name = ""
var body: some View {
VStack(spacing: 20) {
Text("Counter: \(count)")
.font(.largeTitle)
.foregroundColor(.blue)
HStack {
Button("Decrement") {
count -= 1
}
.buttonStyle(.bordered)
Button("Increment") {
count += 1
}
.buttonStyle(.borderedProminent)
}
TextField("Enter name", text: $name)
.textFieldStyle(.roundedBorder)
.padding()
Image(systemName: "star.fill")
.resizable()
.frame(width: 50, height: 50)
.foregroundColor(.yellow)
}
.padding()
}
}
#Preview {
ContentView()
}
3. State Management
// @State for local state
struct CounterView: View {
@State private var count = 0
var body: some View {
Button("Count: \(count)") {
count += 1
}
}
}
// @Binding for two-way data flow
struct ChildView: View {
@Binding var text: String
var body: some View {
TextField("Enter text", text: $text)
}
}
struct ParentView: View {
@State private var inputText = ""
var body: some View {
VStack {
ChildView(text: $inputText)
Text("You typed: \(inputText)")
}
}
}
// @StateObject and @ObservedObject
class DataViewModel: ObservableObject {
@Published var items: [String] = []
@Published var isLoading = false
func loadData() {
isLoading = true
Task {
try? await Task.sleep(for: .seconds(1))
items = ["Item 1", "Item 2", "Item 3"]
isLoading = false
}
}
}
struct DataView: View {
@StateObject private var viewModel = DataViewModel()
var body: some View {
List(viewModel.items, id: \.self) { item in
Text(item)
}
.onAppear {
viewModel.loadData()
}
}
}
// @EnvironmentObject for dependency injection
class AppState: ObservableObject {
@Published var isLoggedIn = false
@Published var username = ""
}
@main
struct MyApp: App {
@StateObject private var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appState)
}
}
}
struct SomeView: View {
@EnvironmentObject var appState: AppState
var body: some View {
Text("User: \(appState.username)")
}
}
4. Navigation
// NavigationStack (iOS 16+)
struct NavigationExample: View {
var body: some View {
NavigationStack {
List(1...20, id: \.self) { item in
NavigationLink("Item \(item)", value: item)
}
.navigationDestination(for: Int.self) { item in
DetailView(itemNumber: item)
}
.navigationTitle("Items")
}
}
}
struct DetailView: View {
let itemNumber: Int
var body: some View {
Text("Detail for item \(itemNumber)")
.navigationTitle("Item \(itemNumber)")
}
}
// Programmatic navigation
struct ProgrammaticNavigation: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Button("Go to Details") {
path.append("details")
}
.navigationDestination(for: String.self) { value in
DetailScreen(onNext: {
path.append("more")
})
}
}
}
}
// Sheets and full screen covers
struct SheetExample: View {
@State private var showSheet = false
@State private var showFullScreen = false
var body: some View {
VStack {
Button("Show Sheet") {
showSheet = true
}
.sheet(isPresented: $showSheet) {
SheetContent()
}
Button("Show Full Screen") {
showFullScreen = true
}
.fullScreenCover(isPresented: $showFullScreen) {
FullScreenContent()
}
}
}
}
5. Lists and Grids
// List
struct UserListView: View {
let users = ["Alice", "Bob", "Charlie"]
var body: some View {
List(users, id: \.self) { user in
HStack {
Image(systemName: "person.circle")
Text(user)
Spacer()
Image(systemName: "chevron.right")
}
}
}
}
// LazyVStack for custom scrolling
struct LazyScrollView: View {
var body: some View {
ScrollView {
LazyVStack(spacing: 10) {
ForEach(0..<100) { index in
CardView(number: index)
}
}
}
}
}
// LazyVGrid for grid layout
struct GridView: View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(0..<20) { index in
RoundedRectangle(cornerRadius: 10)
.fill(Color.blue)
.frame(height: 100)
.overlay(Text("\(index)"))
}
}
.padding()
}
}
}
6. Networking
// API Service
struct User: Codable, Identifiable {
let id: Int
let name: String
let email: String
}
class APIService {
static let shared = APIService()
func fetchUsers() async throws -> [User] {
guard let url = URL(string: "https://api.example.com/users") else {
throw URLError(.badURL)
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
let users = try JSONDecoder().decode([User].self, from: data)
return users
}
func createUser(_ user: User) async throws -> User {
guard let url = URL(string: "https://api.example.com/users") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(user)
let (data, _) = try await URLSession.shared.data(for: request)
return try JSONDecoder().decode(User.self, from: data)
}
}
// ViewModel with async data
@MainActor
class UsersViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
@Published var error: String?
func loadUsers() async {
isLoading = true
error = nil
do {
users = try await APIService.shared.fetchUsers()
} catch {
self.error = error.localizedDescription
}
isLoading = false
}
}
// View with async data
struct UsersView: View {
@StateObject private var viewModel = UsersViewModel()
var body: some View {
Group {
if viewModel.isLoading {
ProgressView()
} else if let error = viewModel.error {
Text("Error: \(error)")
} else {
List(viewModel.users) { user in
VStack(alignment: .leading) {
Text(user.name).font(.headline)
Text(user.email).font(.caption)
}
}
}
}
.task {
await viewModel.loadUsers()
}
}
}
7. Core Data
import CoreData
// Create Core Data stack
class PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init() {
container = NSPersistentContainer(name: "Model")
container.loadPersistentStores { description, error in
if let error = error {
fatalError("Core Data failed: \(error)")
}
}
}
}
// Use in SwiftUI
@main
struct MyApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext,
persistenceController.container.viewContext)
}
}
}
// Fetch data
struct ItemsView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)]
) private var items: FetchedResults<Item>
var body: some View {
List {
ForEach(items) { item in
Text(item.name ?? "")
}
.onDelete(perform: deleteItems)
}
.toolbar {
Button("Add") {
addItem()
}
}
}
private func addItem() {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
newItem.name = "New Item"
try? viewContext.save()
}
private func deleteItems(offsets: IndexSet) {
offsets.map { items[$0] }.forEach(viewContext.delete)
try? viewContext.save()
}
}
8. Animations
struct AnimationExamples: View {
@State private var scale: CGFloat = 1.0
@State private var rotation: Double = 0
@State private var offset: CGFloat = 0
var body: some View {
VStack(spacing: 30) {
// Scale animation
Circle()
.fill(Color.blue)
.frame(width: 100, height: 100)
.scaleEffect(scale)
.onTapGesture {
withAnimation(.spring(response: 0.5, dampingFraction: 0.5)) {
scale = scale == 1.0 ? 1.5 : 1.0
}
}
// Rotation animation
Rectangle()
.fill(Color.green)
.frame(width: 100, height: 100)
.rotationEffect(.degrees(rotation))
.onTapGesture {
withAnimation(.linear(duration: 1)) {
rotation += 360
}
}
// Offset animation
RoundedRectangle(cornerRadius: 10)
.fill(Color.red)
.frame(width: 100, height: 100)
.offset(x: offset)
.onTapGesture {
withAnimation(.easeInOut) {
offset = offset == 0 ? 100 : 0
}
}
}
}
}
9. Custom Views and Modifiers
// Custom view
struct CardView<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
content
.padding()
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 5)
}
}
// Custom modifier
struct PrimaryButtonStyle: ViewModifier {
func body(content: Content) -> some View {
content
.font(.headline)
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(10)
}
}
extension View {
func primaryButtonStyle() -> some View {
modifier(PrimaryButtonStyle())
}
}
// Usage
Button("Click Me") {
print("Tapped")
}
.primaryButtonStyle()
10. Testing
import XCTest
@testable import MyApp
class ViewModelTests: XCTestCase {
var viewModel: DataViewModel!
override func setUp() {
super.setUp()
viewModel = DataViewModel()
}
func testLoadData() async {
await viewModel.loadData()
XCTAssertFalse(viewModel.items.isEmpty)
XCTAssertFalse(viewModel.isLoading)
}
}
// UI Tests
class UITests: XCTestCase {
func testButtonTap() {
let app = XCUIApplication()
app.launch()
let button = app.buttons["Increment"]
button.tap()
let label = app.staticTexts["Counter: 1"]
XCTAssertTrue(label.exists)
}
}
💡 Best Practices:
- Use @State for view-local state
- Use @StateObject for view model instances
- Leverage SwiftUI's declarative syntax
- Follow Apple's Human Interface Guidelines
- Use async/await for networking
- Implement proper error handling
- Write unit and UI tests
- Use preview providers for faster development
Conclusion
Swift and SwiftUI provide a modern, efficient way to build iOS applications. Master these concepts to create beautiful, performant apps that follow Apple's design principles and best practices.