Creating a Bookmarking App with SwiftUI and Core Data

In this comprehensive guide, we’ll walk through creating a simple yet functional bookmarking app using SwiftUI for the user interface and Core Data for persistent storage. Bookmarking apps allow users to save links, articles, and other useful information for later access, making them a handy tool for managing online content.

Why SwiftUI and Core Data?

  • SwiftUI: Provides a declarative and modern approach to building UI, making development faster and more intuitive.
  • Core Data: Offers a robust framework for managing the data model, ensuring data persistence and efficient storage.

Project Setup

Start by creating a new Xcode project:

  1. Open Xcode and select “Create a new Xcode project.”
  2. Choose the “App” template under the iOS tab.
  3. Enter a product name (e.g., “Bookmarker”), select SwiftUI for the interface, and check the “Use Core Data” box.
  4. Save the project in your desired location.

Defining the Core Data Entity

First, define the data model in Core Data. In Xcode, open the .xcdatamodeld file. Click “Add Entity” and name it Bookmark.

  1. Add attributes to the Bookmark entity:
    • title: String (Represents the title of the bookmark)
    • url: String (Represents the URL of the bookmark)
    • timestamp: Date (Represents when the bookmark was created)
  2. Ensure that url and title are set to be non-optional and are indexed. timestamp will also be non-optional.

After defining the entity, Xcode will generate a managed object subclass automatically (if you select ‘Codegen’ to ‘Class Definition’). This will be available to your code automatically.

Creating the SwiftUI Views

1. BookmarkListView

The main view that displays the list of bookmarks.

import SwiftUI
import CoreData

struct BookmarkListView: View {
    @Environment(\\.managedObjectContext) private var viewContext
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \\Bookmark.timestamp, ascending: false)],
        animation: .default)
    private var bookmarks: FetchedResults

    @State private var isAddingBookmark = false

    var body: some View {
        NavigationView {
            List {
                ForEach(bookmarks) { bookmark in
                    NavigationLink(destination: BookmarkDetailView(bookmark: bookmark)) {
                        VStack(alignment: .leading) {
                            Text(bookmark.title ?? "Untitled")
                                .font(.headline)
                            Text(bookmark.url ?? "No URL")
                                .font(.subheadline)
                                .foregroundColor(.gray)
                        }
                    }
                }
                .onDelete(perform: deleteBookmarks)
            }
            .navigationTitle("Bookmarks")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: { isAddingBookmark = true }) {
                        Label("Add Bookmark", systemImage: "plus")
                    }
                }
            }
            .sheet(isPresented: $isAddingBookmark) {
                AddBookmarkView()
            }
        }
    }

    private func deleteBookmarks(offsets: IndexSet) {
        withAnimation {
            offsets.map { bookmarks[$0] }.forEach(viewContext.delete)
            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}

2. AddBookmarkView

A sheet view to add a new bookmark.

import SwiftUI
import CoreData

struct AddBookmarkView: View {
    @Environment(\\.managedObjectContext) private var viewContext
    @Environment(\\.dismiss) var dismiss

    @State private var title: String = ""
    @State private var url: String = ""

    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Bookmark Details")) {
                    TextField("Title", text: $title)
                    TextField("URL", text: $url)
                        .keyboardType(.URL)
                }
            }
            .navigationTitle("Add Bookmark")
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Cancel") {
                        dismiss()
                    }
                }
                ToolbarItem(placement: .confirmationAction) {
                    Button("Save") {
                        addBookmark()
                    }
                    .disabled(title.isEmpty || url.isEmpty)
                }
            }
        }
    }

    private func addBookmark() {
        withAnimation {
            let newBookmark = Bookmark(context: viewContext)
            newBookmark.timestamp = Date()
            newBookmark.title = title
            newBookmark.url = url

            do {
                try viewContext.save()
                dismiss()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}

3. BookmarkDetailView

A detailed view for displaying bookmark information. Users can tap on URL to launch it in the default browser of iOS. This can be accomplished with a custom function.

import SwiftUI

struct BookmarkDetailView: View {
    let bookmark: Bookmark

    var body: some View {
        VStack(alignment: .leading) {
            Text(bookmark.title ?? "Untitled")
                .font(.largeTitle)
                .padding()

            Text("URL: \(bookmark.url ?? "No URL")")
                .padding()
                .onTapGesture {
                    openURL(urlString: bookmark.url)
                }
            Spacer()
        }
        .navigationTitle("Bookmark Detail")
    }

    // Function to Open URL
    private func openURL(urlString: String?) {
        guard let urlString = urlString, let url = URL(string: urlString) else {
            return
        }
        if UIApplication.shared.canOpenURL(url) {
            UIApplication.shared.open(url)
        }
    }
}

Integrate Core Data

The ContentView or main App file needs to inject the environment for CoreData.

import SwiftUI

@main
struct BookmarkerApp: App {
    let persistenceController = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            BookmarkListView()
                .environment(\\.managedObjectContext, persistenceController.container.viewContext)
        }
    }
}

Running the App

Run the app in the Xcode simulator or on a physical device.

  • The BookmarkListView should display any saved bookmarks or a list which is initially empty.
  • Tap the “+” button to open the AddBookmarkView and add a new bookmark.
  • Newly added bookmarks should appear in the BookmarkListView.
  • You can also tap into a list and tap the link to launch that link in the browser

Additional Features

  • Searching: Implement a search feature using @State and filtering logic within the BookmarkListView.
  • Categorizing Bookmarks: Add categories to your data model to allow organization.
  • iCloud Syncing: Integrate CloudKit to sync bookmarks across devices.
  • User Authentication: Include user authentication for personal bookmark management.

Conclusion

This guide covered building a bookmarking app using SwiftUI for the UI and Core Data for data persistence. Leveraging the capabilities of both technologies ensures an efficient and scalable development process. Experimenting with additional features enhances the utility and functionality of the app, making it a valuable tool for users to manage their online bookmarks.