Time Zones App part 3: Persisting Timezones with CoreData

Swift UI - Time Zone App Dec 04, 2019

In Last week’s article, we started using remote data to feed our app with timezones fetched from the internet. In this one, we will need to save added TimeZones in the database using Core Data.

In the following section, I will show you how to setup core data in a swiftUI app from scratch. Do this if you didn’t check the core data box when created your project otherwise jump directly to the 'Create Entity Section'.

Download the final project here

Core Data Setup

  1. App Delegate
    First import CoreData in your AppDelegate.swift file with this line below the UIKit import:
    Import CoreData

then add the following code below everything:

		lazy var persistentContainer: NSPersistentContainer = {
	
	    	let container = NSPersistentContainer(name: "ProgressTracker")
	    	container.loadPersistentStores(completionHandler: { (storeDescription, error) in
	        	if let error = error as NSError? {
	
	            	fatalError("Unresolved error \(error), \(error.userInfo)")
	        	}
	    	})
	    	return container
		}()


The above code just creates a PersistentContainer.

NSPersistentContainer simplifies the creation and management of the Core Data stack by handling the creation of the managed object model (NSManagedObjectModel), persistent store coordinator (NSPersistentStoreCoordinator), and the managed object context (NSManagedObjectContext).

Then below it, add the following:

    func saveContext () {
    	let context = persistentContainer.viewContext
    	if context.hasChanges {
        	do {
            	try context.save()
        	} catch {
            	let nserror = error as NSError
            	fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
        	}
    	}
	}

This is an helper method that just checks whether there’s changes and saves them.

2. Scene Delegate
In your scene delegate, in the very first method (the willConnectTo session), add the following line above the ContentView instantiation:

let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

We just get the context we’ve just created in the delegate. Then add this modifier directly after ContentView() like this:

let contentView = ContentView().environment(\.managedObjectContext, context)

This just saved the context to the environment to make it available to other parts of the project.
Finally, put the following line in sceneDidEnterBackground(_ scene: UIScene)_ located at the bottom of the class:

(UIApplication.shared.delegate as? AppDelegate)?.saveContext()

That’s it, your project is now ready to use Core Data. Let’s get started.

Data Model File and Entity Creation.

Data Model File

Do this to create a core data model file.

Right click the project folder and select New File, scroll down to the core data section and choose Data Model, click next and give the file the same name as your project again.

Create Entity

In this project, we will only need one entity. To create it, select the data model file you’ve just created, then click the add button located at the bottom-left corner and rename the Entity to SavedTimeZone.

Then, while that entity is selected, click the add button in the attributes section to add an attributes.

Add those 5 attributes. When you’re done, select the inspector on the right and set the module to the current project module and the codegen to Manual/None because we want to control the managedObject ourselves

NSManagedObject

It’s now time to create the managed object for our core data entity. In the Model folder, create a swift file named SavedTimeZone and put the following code inside:

import Foundation
import CoreData

public class SavedTimeZone: NSManagedObject, Identifiable{
     @NSManaged public var id: UUID?
     @NSManaged public var createdAt: Date?
     @NSManaged public var area: String?
     @NSManaged public var location: String?
     @NSManaged public var utcOffset: String?
}

And that’s our managed object class.

Now open the AreaLocationView.swift file and put this at the top of the file:

 @Environment(\.managedObjectContext) var managedObjectContext

This is the context we set in SceneDelegate and we will need it to perform core data operation, then in the getTimeZone’s completion block above self.isLoading = false, add the following:


 // Core data
 let newTimeZone = SavedTimeZone(context: self.managedObjectContext)
     newTimeZone.createdAt = Date()
     newTimeZone.id = timeZone.id
     newTimeZone.area = timeZone.area
     newTimeZone.location = timeZone.location
     newTimeZone.utcOffset = timeZone.utcOffset
	 try? self.managedObjectContext.save()

The above code creates a new object of our managedObject and persists it to the database. Try running the app and if it doesn’t crash, it means you did everything correctly, otherwise you will need to verify if your setup is correct and same as mine.

Let’s now display the saved timezones in our SavedTimeZonesView. To do so, add the following at the top inside the SavedTimeZonesView struct:

 @FetchRequest(
          entity: SavedTimeZone.entity(),
          sortDescriptors: [
              NSSortDescriptor(keyPath: \SavedTimeZone.createdAt, ascending: true),
          ]
      ) var timezones: FetchedResults<SavedTimeZone>

Remove the .reversed() from the timezones parameter in the ForEach and In the TimeZoneView Struct replace the following:

  var timeZone: TimeZone

With this:

var timeZone: SavedTimeZone

Then replace the HStack returned in the body with the following:

HStack{
            VStack(alignment: .leading) {
                Text(timeZone.area ?? "").font(.system(size: 15, weight: Font.Weight.black))
                Text(timeZone.location ?? "").foregroundColor(Color.gray)
            }
            Spacer()
            Text(timeZone.utcOffset ?? "").font(.system(size: 20, weight: Font.Weight.black))
        }.frame(maxWidth: .infinity)
            .padding(30)
            .background(Color.white)
            .cornerRadius(20)
            .shadow(color: Color.gray.opacity(0.2), radius: 8, x: 0, y: 8)
            .padding(.horizontal)
    }

The code is the same except the places where we are using timezone’s properties which are optionals, so we need to take care of them in case any of them is nil. Open contentView and remove the timezones parameter from the SavedTimeZones call.

Now run the app and everything should run perfectly and the data will be persisted to the database.

Minor Refactoring

This part will be focused on tidying up the UI and the code that displays the date and time. Right now the saved time zones shows only the UTC offset, without knowing the UTC time, it will be hard for the user to understand what going on. Let’s fix that, shall we!

First, you will be required to rename the TimeZone file and struct to AddedTimeZone to avoid conflict with the built-in TimeZone struct that we will use shortly.

Create a folder named, Extensions and a swift file named DateExt, then paste the following code inside:

import Foundation

extension Date{
    
    var formatted: String {
        let formatter = DateFormatter()
        formatter.dateFormat = "E, d MMMM yyyy"
        return formatter.string(from: self)
    }
    
     func getComponent(format: String) -> Double {
        let formatter = DateFormatter()
        formatter.dateFormat = format
        formatter.timeZone = TimeZone(identifier: "UTC")
        let component = formatter.string(from: self)
        
        if let value = Double(component) {
            return value
        }
        return 0.0
    }  
}

This is just a helper that we will use to shorten the code that’s used to display the date. In contentView, find the Header struct and replace the content of its body with this:

        VStack{
            Text("Coordinated Universal Time (UTC)").font(.system(size: 20)).bold()
            Text("\(Date().formatted)")
        }.frame(height: self.headerHeight)

Then open the WatchView.swift file and replace everything in .onAppear with the following:

 let date = Date()
            self.hours = date.getComponent(format: "HH")
            self.minutes = date.getComponent(format: "mm")
            self.seconds = date.getComponent(format: "ss")

            Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
                let date = timer.fireDate

                self.hours = date.getComponent(format: "HH")
                self.minutes = date.getComponent(format: "mm")
                self.seconds = date.getComponent(format: "ss")
            }

You can now delete the method that was used before. Now if you run the app, the default time will be UTC time.

This is the end of this series folks. Of course, there’s a big big room for improvements, a user should have the ability to delete one or multiple timezones and so on...So, I challenge you to implement the deletion.

Please share this article and subscribe for more.

John K

I am a software developer and code enthusiast. Do you want to work with me, have a suggestion or a request? Feel free to contact me at [email protected] or https://twitter.com/liquidcoder