A new Side Project has recently been launched to host the content announced in WWDC19. This article mainly describes the combination of SwiftUI and Core Data, as well as my problems and thoughts in the first chapter.

preface

Core Data is a love-hate thing, loving it for its native support and perfect integration with Xcode, and hating it for causing unpredictable problems in extreme cases, such as unavoidable initialization time consumption and various main-thread dependency operations. As far as I know, Watermelon Video and Toutiao used to rely heavily on Core Data, but both have been withdrawn due to “certain performance” issues.

Why do I stick with Core Data when I’ve learned the hard way? As I said just now, Core Data was removed from these two apps because of “certain performance” problems, but the general side project can ignore these problems. In addition, WWDC19 has four sessions related to Core Data, so the star halo is enough!

Core DataEncapsulation use of

Create the model

Let’s look at the finished image first,

This is a very simple list, in UIKit we only need a UITableView operation to complete the list, the code is only a few dozen lines, wrapped with SwiftUI, the main list can be completed in less than ten lines, as follows:

struct MASSquareListView : View {@State var articleManage = AritcleManager()
    @State var squareListViewModel: MASSquareListViewModel
    
    var body: some View {
        List(self.articleManage.articles, id: \.createdAt) { article in
            MASSquareNormalCellView(article: article)
                .padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5))}}}Copy the code

Now, assuming that our list is ready, let’s think about the data we need to enter in the list and parse it with another graph:

Each Cell needs to input Data such as “profile picture”, “creation time” and “content”. In this article, we only consider the first step of the interaction between storage and Core Data, and how to make Core Data into CloudKit or its own server will be expanded in the subsequent articles.

As you can see from the figure, our Model belongs to the NSManagerObjectModel, and this article describes how to create the.xcDatamodeld file.

Once created, we can define the entity attributes as shown in the following figure based on the UI composition of the previous analysis:

  • avatarColor: The picture is divided into two parts: “color” and “picture”, each picture is transparent channelpngType picture. Users can only use a few colors defined in the app;
  • avatarImage: as above;
  • content: content, this field is used in long text on the server sideStringKeep in line;
  • createdAt: creation time;
  • type: Consider that each subsequent tweet could be in a different shape, such as belt or notflaglink;
  • uid: The user ID needed for the tweet. The field described in this article is a redundant field, you do not need to add, before considering the follow-up work, it is ok to add later.

We can either have Core Data automatically generate code that matches the model or we can write it ourselves. By reading the Core Data book “ObjC China”, you can understand that it won’t be too much work to write matching model codes by yourself, and also deepen your understanding of the process of model generation (previously, in order to save trouble, Core Data was automatically generated, and the model codes completed are as follows:

final class Article: NSManagedObject {
    @NSManaged var content: String
    @NSManaged var type: Int16
    @NSManaged var uid: Int32
    @NSManaged var avatarImage: Int16
    @NSManaged var avatarColor: Int16
    @NSManaged internal var createdAt: Date
}
Copy the code

After the model code is written, go to the entity of the.xcDatamodeld file and select the newly written model class and disable the automatic code generation option for Core Data:

What we are actually doing in this section is defining the entity structure to be stored, in other words, describing the data that you want to store by doing the above.

To create aCore DataStorage structure

In this part, I used to create the memory in the AppDelegate according to the generation template of Xcode, which was oriented to fulfill the requirements. As a result, when I continued to connect and store other entities, the code quality was rather rough. After some learning, I adjusted the direction.

Take a look at the storage structure of Core Data on objC China:

You can have multiple entities, manage the operations of each entity through the context, and then the context interacts with storage through the coordinator, interacts with the underlying database. This diagram is actually very similar to the subsequent process of pushing data into CloudKit, but in this article we will do it in the same way as this diagram of “ObjC China” :

Manage multiple entities through a single context and have only one storage manager. To facilitate subsequent calls to the data management method, and the memory does not need to be created repeatedly, I pulled a singleton to manage:

class MASCoreData {
    static let shared = MASCoreData(a)var persistentContainer: NSPersistentContainer!
    // create a storage container
    class func createMASDataModel(completion: @escaping() - > ()){
        // The name must be the same as the file name of '.xcdatamodeleld '
        let container = NSPersistentContainer(name: "MASDataModel")
        
        container.loadPersistentStores { (_, err) in
            guard err == nil else { fatalError("Failed to load store: \(err!)")}DispatchQueue.main.async {
                self.shared.persistentContainer = container
                completion()
            }
        }
    }
}
Copy the code

For initialization, we can use:

func scene(_ scene: UIScene,
            willConnectTo session: UISceneSession,
            options connectionOptions: UIScene.ConnectionOptions) {
    
    //TODO:This is a bit rude, you can't just create a blank screen if the database fails. This article will focus on the implementation of the requirements, and the rest will be covered in a later article
    MASCoreData.createMASDataModel {
        if let windowScene = scene as? UIWindowScene {
            
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView:
                MASSquareHostView()
                    .environmentObject(MASSquareListViewModel()))self.window = window
            window.makeKeyAndVisible()
        }
    }
}
Copy the code

EnvironmentObject in the code is an addition to the control menu display and hiding that you needed in the previous article, and can be left out in this article. By doing this, we have created a usable memory when our app initializes.

Data interaction

Once you have the model, you have the memory, you have to start adding, deleting, changing and checking. In fact, there have been many articles to explain the implementation of adding, deleting, modifying and checking Core Data, so I won’t expand it here. In my previous Core Data query, I wrote this:

func allxxxModels(a)- > [PJxxxModel] {
    var finalModels = [PJModel] ()let fetchRequest = NSFetchRequest<xxxModel>(entityName: "xxxModel")
    do {
        let fetchedObjects = trycontext? .fetch(fetchRequest).reversed()guardfetchedObjects ! =nil else { return []}
        // Do some data reading operations......
       
        print("Query successful")
        return finalModels
    }
    catch {
        print("Query failed:\(error)")
        return[]}}Copy the code

Before actually at first glance, is also good, I also feel very good, but when I wrote after three or four entities, found that every query method of the new entity needs to replicate before written query methods, change with the parameters, feel some wrong place at the time, because of the repeated work has been doing, what would I do right now?

First of all, every time you create an NSFetchRequest, you have to hardcode the entity name, and you also need to create multiple intermediate entity objects and the intermediate code of the real object model, because the Data fields stored in Core Data all depend on the API model fields. So there is a lot of compatibility code in almost every view query method, which is pretty ugly.

Finally, I encountered the same problem in this project. The second problem is that you have to write two models, otherwise your Core Data model fields will become “gigantic”, so I wrote two models, one for Core Data and one for API model.

The first problem can be solved by agreement:

protocol Managed: class.NSFetchRequestResult {
    static var entityName: String { get }
    static var defaultSortDescriptors: [NSSortDescriptor] { get}}extension Managed {
    static var defaultSortDescriptors: [NSSortDescriptor] {
        return[]}static var sortedFetchRequest: NSFetchRequest<Self> {
        let request = NSFetchRequest<Self>(entityName: entityName)
        request.sortDescriptors = defaultSortDescriptors
        return request
    }
}

extension Managed where Self: NSManagedObject {
    static var entityName: String { returnentity().name! }}Copy the code

In this way, as long as an ns-managed object complies with the Managed protocol, it is possible to obtain the entityName through the entityName property without the need for hard-coded strings. As you can see in the UI diagram, it’s basically sort the tweets in reverse order, so instead of writing sortDescriptors in every NSFetchRequest I’ve given a default implementation, When querying data, you only need to configure it by calling the sortedFetchRequest property.

Now everything is configured except to slice the data into a list for presentation. After retrieving the data from the return value of the allxxxModels() method, manually synchronize the UITableView to reloadData(), But now we are using SwiftUI. If we still use the previous UIKit method, it is definitely not in line with SwiftUI workflow.

If you’ve followed SwiftUI, you’re no doubt familiar with @state, @bindingobject, and @environmentobject. I define these moditudes from a component perspective, but there are other ways to use them. The three attributes I defined in my usage are:

  • @State: transfer of data or state within a component;
  • @BindingObject: data transfer across components;
  • @EnvironmentObject: Data transfer across components. From the name, you can also set some immutable environment values, which we will try to use in the user management section later.

In order to comply with SwiftUI’s official recommendation for data flow processing, we need to define a class that complies with the ObservableObject protocol and send data through this class:

class AritcleManager: NSObject.ObservableObject {@Published var willChange = PassthroughSubject<Void.Never> ()var articles = [Article] () {willSet {
            willChange.send()
        }
    }
}
Copy the code

Note that this is the code I used to migrate from SwiftUI Beta4 to Beta5, and versions prior to SwiftUI Beta5 didn’t work. @published var willChange = PassthroughSubject

() Var willChange = PassthroughSubject

().
,>
,>








The second parameter indicates the error definition when the notification is thrown. If an error is encountered, what type of error should be thrown. This is actually not a good idea, and should be thrown based on the actual problems encountered, which will be improved in future articles.
,>
,>
,>
,>
,>
,>
,>

When we modify articles, we trigger the willSet method to call send() to trigger the notification, and then we listen for the notification elsewhere via @bindobject:

struct MASSquareListView : View {
    // Instantiate it internally, because only the View is used
    @State var articleManage = AritcleManager()
    @State var squareListViewModel: MASSquareListViewModel
    
    var body: some View {
        List(self.articleManage.articles, id: \.createdAt) { article in
            MASSquareNormalCellView(article: article)
                .padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5))}}}Copy the code

So if we could just do what we did before, update the values of articles after we get the data from NSFetchRequest, which is what I did before, but you can’t just put one implementation in multiple projects, right? That would be boring. So in order to better suit the Core Data usage, we use NSFetchedResultsController to manage Data.

Using NSFetchedResultsController to manage Data, we can not ignore the Core Data, the change of the Data to add and delete, only need to pay attention to NSFetchedResultsController proxy approach, which I realize is:

extension AritcleManager: NSFetchedResultsControllerDelegate {
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        articles = controller.fetchedObjects as! [Article]}}Copy the code

I haven’t implemented all the methods, and if we were using a traditional UITableView, we might need to implement the remaining proxy methods. Here, my personal recommendation is that if you have a real need to deal with “certain things”, that every entity is best to be a manager do to NSFetchedResultsControllerDelegate agreement implementation, Because each entity is likely to need to pay attention to each proxy method in NSFetchedResultsControllerDelegate agreement point is different, can’t slap shot to death, what is abstract.

By NSFetchedResultsController data changes after listening, at the time of instantiation AritcleManager, catch up on some configuration work to do:

class AritcleManager: NSObject.ObservableObject {@Published var willChange = PassthroughSubject<Void.Never> ()var articles = [Article] () {willSet {
            willChange.send()
        }
    }
    fileprivate var fetchedResultsController: NSFetchedResultsController<Article>
    
    override init() {

        let request = Article.sortedFetchRequest
        request.fetchBatchSize = 20
        request.returnsObjectsAsFaults = false
        self.fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: MASCoreData.shared.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
        
        super.init()
        
        fetchedResultsController.delegate = self
        
        // Return immediately after executing the method
        try! fetchedResultsController.performFetch() articles = fetchedResultsController.fetchedObjects! }}Copy the code

With this code, we’re done. When changes are made to the Article entity in Core Data, the changes are sent directly to all external listeners.

Let’s now look at how to insert a piece of data. Here’s what I used to do:

func addxxxModel(models: [xxxModel]) -> Bool{
    
    for model in models {
        let entity = NSEntityDescription.insertNewObject(forEntityName: "xxxModel", into: context!) as! xxxModel
        
        // Do some final preparatory work before insertion
    }
    do {
        trycontext? .save()print("Saved successfully")
        return true
    } catch {
        print("Cannot save:\(error)")
        return false}}Copy the code

It can be seen when inserting data or have to rely on the context to do management, according to our previous idea, through NSFetchedResultsController monitored data change is in order to achieve don’t need to call the fetch method through the context every time pull the latest data, But inserting the data must be done “manually” and must display the call.

Thus, we can encapsulate this “repetitive” operation, instead of writing an insert method for each entity as I did before:

extension NSManagedObjectContext {
    func insertObject<T: NSManagedObject>(a) -> T where T: Managed {
        guard let obj = NSEntityDescription.insertNewObject(forEntityName: T.entityName, into: self) as? T else { fatalError("error object type")}return obj
    }
}
Copy the code

Using generics to qualify callers that return objects within a method are of type NS-managed object. Using WHERE to qualify callers must follow the Managed protocol. Therefore, we can modify the Core Data model of Article to:

final class Article: NSManagedObject {
    @NSManaged var content: String
    @NSManaged var type: Int16
    @NSManaged var uid: Int32
    @NSManaged var avatarImage: Int16
    @NSManaged var avatarColor: Int16
    @NSManaged internal var createdAt: Date
    
    static func insert(viewModel: Article.ViewModel) -> Article {
        
        let context = MASCoreData.shared.persistentContainer.viewContext
        
        let p_article: Article = context.insertObject()
        p_article.content = viewModel.content
        p_article.avatarColor = Int16(viewModel.avatarColor)
        p_article.avatarImage = Int16(viewModel.avatarImage)
        p_article.type = Int16(viewModel.type)
        p_article.uid = Int32(2015011206)
        p_article.createdAt = Date(a)return p_article
    }
}
Copy the code

Afterword.

Here you will find that, we didn’t actually SwiftUI with Core Data do other context dependent on work, this is because we use the NSFetchedResultsController Article entity to dynamic monitoring Data changes, The updated data is then sent by calling the send() method via the @publisher embellished object.

The Combine used in this article is mainly reflected in the fact that Data acquisition and update of Core Data do not require active notification of the UI. Of course, it is ok if you insist that these things do not need to Combine support, because it can be done based on Notification. More details about Combine will be refined as the project progresses.

Note: Some of the content in this article may not conform to final or current practices as the project continues to progress.

The resources

Core Data

Project address: Masq iOS client

SwiftUI: How to integrate with Core Data?