back

Word card game for iOS

I had another idea for an application that would help me learning languages by guessing word translations and store new words that I learn.

Problem

- the application should be able to store the data - the data should include origin of word, translation and short description to know the meaning better (optional) - as I wanted this app to be really handy I decided to try to implement it for mobile

Tech

- SwiftUI, iOS 15.6+ - Combine library - Core Data API

Structure

Firstly, I want to establish the application structure with proper (from my sight) folder architecture. I prefer to stick to the following:
Root
- Core
- - Home
- - - Views
- - - - HomeView
- - - ViewModels
- - - - HomeViewModel
- Models
- - WordEntity
- Extensions
- Services
This structure helps to separate logic properly and not mess up with responsibilities. Since we stick to MVVM architecture here, it makes sense to follow it dividing each app into view, view model and model.

Core

I have decided to start with a simple service that is going to be the core of the application. This service is a straight-forward connection to Core Data via API with all CRUD operations.
import Foundation
import CoreData
import Combine

class Storage {
  private let container: NSPersistentContainer
  private let wordsContainerName: String = "WordContainer"
  private let wordsEntityName: String = "WordEntity"

  @Published var entities: [WordEntity] = []
  
  private var cancellables = Set<AnyCancellable>()
  
  init() {
    container = NSPersistentContainer(name: wordsContainerName)
    container.loadPersistentStores {_,_ in
      self.error = "Failed to load store"
    }

    self.fetch()
  }
  
  func update(id: UUID, word: Word) {
    delete(id: id)
    add(word)
    save()
  }

  func add(_ word: Word) -> Void {
    let entity: WordEntity = generateWordEntity()

    assignToEntity(entity: entity, word: word)

    save()
    refetch()
  }

  func delete(id: UUID) -> Void {
    let entity = entities.first { $0.id == id }

    if let entityUnwrapped = entity {
      deleteFromStorage(entityUnwrapped)
      save()
    }
  }

  func flush() -> Void {
    entities.forEach { deleteFromStorage($0) }
    entities.removeAll()

    save()
  }

  private func deleteFromStorage(_ entity: WordEntity) {
    container.viewContext.delete(entity)
  }

  private func generateWordEntity() -> WordEntity {
    return WordEntity(context: container.viewContext)
  }

  private func assignToEntity(entity: WordEntity, word: Word) -> Void {
    entity.id = word.id
    entity.info = word.info
    entity.translation = word.translation
    entity.word = word.word
  }

  private func fetch() -> Void {
    let request = NSFetchRequest<WordEntity>(entityName: wordsEntityName)

    do {
      entities = try container.viewContext.fetch(request)
    } catch _ {
      self.error = "Failed to fetch entities from store"
    }
  }

  private func save() {
    do {
      try container.viewContext.save()
    } catch _ {
      self.error = "Failed to save entities to store"
    }
  }
}
In simple words we need to initialise the Persistence Container with a certain name to identify stored entities. In the context of this container we may fetch currently stored in memory entities by performing fetch, or create new entities by providing this container's context into entity initialisation - you may find it in add function.
Once we fetch the entities we need to store them in a variable that is going to be published, so then we can subscribe to all the changes of this specific variable in our view model.
Since I have mentioned a view model, I would also like to share a sample of what I did there:
class HomeViewModel: ObservableObject {
  @Published var words: [WordEntity] = []
  
  private let wordStorageService = Storage()
  private let cancellables = Set<AnyCancellable>()  

  init() {
    addSubscribers()
  }

  private func addSubscribers() {
    wordStorageService.$entities
      .sink { [weak self] (entities: [WordEntity]) in
        self.words.append(entities)
      }
      .store(in: $cancellables)
  }
The logic here is to subscribe for changes of entities and set it to the local variable that is going to be published again and accessed in the view. Here we have a place to manipulate this data - for e.g. sort, search, convert and etc.

Conclusion

That is pretty much the core of the application the rest is just a sugar and interfaces that you can easily find in the source code
See you!