Creating a Photo Gallery App in SwiftUI

Creating a photo gallery application is a great way to dive into SwiftUI, Apple’s declarative UI framework. A photo gallery not only demonstrates fundamental UI concepts but also incorporates image handling, scrolling, and data management. In this comprehensive guide, we’ll walk you through the steps to build a fully functional photo gallery app using SwiftUI.

Why Build a Photo Gallery App with SwiftUI?

  • Modern UI Framework: SwiftUI offers a declarative and concise way to define user interfaces.
  • Ease of Use: Simplifies the process of building complex layouts and UI elements.
  • Cross-Platform Compatibility: SwiftUI works across all Apple platforms, including iOS, macOS, watchOS, and tvOS.
  • Real-World Application: Building a photo gallery demonstrates practical skills in data management, UI design, and image handling.

Prerequisites

Before you start, ensure you have:

  • Xcode 13 or later installed on your macOS.
  • A basic understanding of Swift programming.
  • Familiarity with SwiftUI concepts such as Views, Layouts, and State.

Step 1: Setting Up the Project

  1. Open Xcode and select “Create a new Xcode project.”
  2. Choose the “iOS” tab and select “App.” Click “Next.”
  3. Enter “PhotoGallery” as the project name, select “SwiftUI” as the interface, and click “Next.”
  4. Choose a location to save the project and click “Create.”

Step 2: Defining the Data Model

Create a data model to represent each photo in the gallery. This model will store the image name and a unique identifier.

Create a new Swift file named Photo.swift:


import SwiftUI

struct Photo: Identifiable {
    let id = UUID()
    let imageName: String
}

Step 3: Populating the Gallery Data

Create an array of Photo objects to populate the gallery. For this example, we’ll use images included in the asset catalog. Make sure to add some images to your Assets.xcassets folder within your project and name them (e.g., “photo1”, “photo2”, “photo3”).


import SwiftUI

struct Photo: Identifiable {
    let id = UUID()
    let imageName: String
}

let galleryPhotos = [
    Photo(imageName: "photo1"),
    Photo(imageName: "photo2"),
    Photo(imageName: "photo3"),
    Photo(imageName: "photo4"),
    Photo(imageName: "photo5"),
    Photo(imageName: "photo6"),
    Photo(imageName: "photo7"),
    Photo(imageName: "photo8"),
    Photo(imageName: "photo9"),
    Photo(imageName: "photo10")
]

Step 4: Creating the Photo Gallery View

Create the main view for the photo gallery using ScrollView and LazyVGrid for an efficient grid layout.

Open ContentView.swift and replace the default code with the following:


import SwiftUI

struct ContentView: View {
    let columns: [GridItem] = [
        GridItem(.flexible()),
        GridItem(.flexible())
    ]

    var body: some View {
        NavigationView {
            ScrollView {
                LazyVGrid(columns: columns, spacing: 10) {
                    ForEach(galleryPhotos) { photo in
                        Image(photo.imageName)
                            .resizable()
                            .scaledToFill()
                            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 150)
                            .clipped()
                            .cornerRadius(10)
                            .padding(5)
                    }
                }
                .padding(.horizontal)
            }
            .navigationTitle("Photo Gallery")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Here’s what the code does:

  • NavigationView provides a navigation bar for the view.
  • ScrollView enables vertical scrolling for the gallery.
  • LazyVGrid creates a grid layout that efficiently loads images as the user scrolls.
  • ForEach iterates through the galleryPhotos array.
  • Image(photo.imageName) loads each image from the asset catalog.
  • Modifiers such as resizable, scaledToFill, frame, clipped, and cornerRadius are used to style the images.

Step 5: Adding a Detail View

To display each photo in full screen when tapped, create a detail view. Create a new SwiftUI View file named PhotoDetailView.swift:


import SwiftUI

struct PhotoDetailView: View {
    let photo: Photo

    var body: some View {
        ZStack {
            Color.black.edgesIgnoringSafeArea(.all) // Background Color

            Image(photo.imageName)
                .resizable()
                .scaledToFit()
                .edgesIgnoringSafeArea(.all)
        }
        .navigationTitle("Photo Details")
        .navigationBarTitleDisplayMode(.inline) // Keeps title in the same line
    }
}

Modify the ContentView.swift to link the photo items to this detail view. Use NavigationLink for this purpose:


import SwiftUI

struct ContentView: View {
    let columns: [GridItem] = [
        GridItem(.flexible()),
        GridItem(.flexible())
    ]

    var body: some View {
        NavigationView {
            ScrollView {
                LazyVGrid(columns: columns, spacing: 10) {
                    ForEach(galleryPhotos) { photo in
                        NavigationLink(destination: PhotoDetailView(photo: photo)) {
                            Image(photo.imageName)
                                .resizable()
                                .scaledToFill()
                                .frame(minWidth: 0, maxWidth: .infinity, minHeight: 150)
                                .clipped()
                                .cornerRadius(10)
                                .padding(5)
                        }
                    }
                }
                .padding(.horizontal)
            }
            .navigationTitle("Photo Gallery")
        }
    }
}

With this, tapping an image will navigate to a full-screen view displaying the selected photo.

Step 6: Improving Performance with Asynchronous Images

To load images asynchronously and prevent UI blocking, especially for larger images, use AsyncImage.


import SwiftUI

struct AsyncPhotoView: View {
    let photo: Photo

    var body: some View {
        AsyncImage(url: URL(string: getImageUrl(photoName: photo.imageName))) { phase in
            switch phase {
            case .empty:
                ProgressView() // Display a loading indicator
            case .success(let image):
                image
                    .resizable()
                    .scaledToFill()
                    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 150)
                    .clipped()
                    .cornerRadius(10)
                    .padding(5)
            case .failure:
                Image(systemName: "photo") // Display a placeholder if image fails to load
                    .resizable()
                    .scaledToFit()
                    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 150)
                    .padding(5)
                    .foregroundColor(.gray)

            @unknown default:
                EmptyView()
            }
        }
    }

    //Helper function - you may need to adjust it depending on your set up:
    private func getImageUrl(photoName: String) -> String? {
        guard let url = Bundle.main.url(forResource: photoName, withExtension: "jpg") else {
            return nil
        }
        return url.absoluteString
    }
}

//Modify ContentView as below:
struct ContentView: View {
    let columns: [GridItem] = [
        GridItem(.flexible()),
        GridItem(.flexible())
    ]

    var body: some View {
        NavigationView {
            ScrollView {
                LazyVGrid(columns: columns, spacing: 10) {
                    ForEach(galleryPhotos) { photo in
                        NavigationLink(destination: PhotoDetailView(photo: photo)) {
                            AsyncPhotoView(photo: photo)
                        }
                    }
                }
                .padding(.horizontal)
            }
            .navigationTitle("Photo Gallery")
        }
    }
}

Note: Modify the PhotoDetailView to use AsyncImage instead of Image.

Key improvements in this snippet:

  • Uses AsyncImage to handle the asynchronous loading of images, showing a progress view while loading, a default image in case of failure and managing memory more efficiently.

Step 7: Adding Animations

Let’s add a subtle animation when tapping the photo. Here’s how you can achieve that:


import SwiftUI

struct ContentView: View {
    let columns: [GridItem] = [
        GridItem(.flexible()),
        GridItem(.flexible())
    ]

    @State private var selectedPhotoId: UUID? = nil
    @State private var isAnimating: Bool = false

    var body: some View {
        NavigationView {
            ScrollView {
                LazyVGrid(columns: columns, spacing: 10) {
                    ForEach(galleryPhotos) { photo in
                        Button(action: {
                            selectedPhotoId = photo.id
                            isAnimating = true
                            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                                isAnimating = false
                            }
                        }) {
                            AsyncPhotoView(photo: photo)
                                .scaleEffect(isAnimating && selectedPhotoId == photo.id ? 0.9 : 1.0)
                                .animation(.easeInOut(duration: 0.2), value: isAnimating)
                        }
                        .buttonStyle(PlainButtonStyle())
                    }
                }
                .padding(.horizontal)
            }
            .navigationTitle("Photo Gallery")
        }
    }
}

Changes explained:

  • The @State variable isAnimating determines if the scaling animation is active, initialized to false.
  • We introduce a @State variable selectedPhotoId to track which photo initiated the animation, enabling targeted animation.
  • The scaling effect is now triggered conditionally, animating only the selected photo via checking selectedPhotoId == photo.id .
  • A DispatchQueue.main.asyncAfter block ensures isAnimating returns to false briefly after being set true, starting and completing the animation sequence.
  • The scaling and animation effects are contained directly in AsyncPhotoView
  • The Button includes a .buttonStyle(PlainButtonStyle()) modifier, necessary for maintaining the expected visual presentation by preventing button-related styling (like highlights).

Conclusion

Building a photo gallery app in SwiftUI is a fantastic way to learn and apply key UI development concepts. You’ve covered project setup, data modeling, UI layout with ScrollView and LazyVGrid, navigation with NavigationLink, detail views, asynchronous image loading, and animations. These skills will be invaluable as you tackle more complex SwiftUI projects. Feel free to extend this app further by adding features like image editing, sharing, and cloud storage integration!