12/08/2018, 10:52

Đồng bộ dữ liệu Core Data với Parse Service (Phần 1)

Ở trong bài viết trước tôi đã trình bày về cách tạo 1 ứng dụng lưu dữ liệu trực tiếp lên Parse Service, việc này giúp cho dữ liệu luôn được đồng bộ giữa nhiều thiết bị, tuy nhiên việc này có hạn chế là chương trình không thể hoạt động nếu không có mạng internet. Trên thực tế, các chương trình đều ...

Ở trong bài viết trước tôi đã trình bày về cách tạo 1 ứng dụng lưu dữ liệu trực tiếp lên Parse Service, việc này giúp cho dữ liệu luôn được đồng bộ giữa nhiều thiết bị, tuy nhiên việc này có hạn chế là chương trình không thể hoạt động nếu không có mạng internet.

Trên thực tế, các chương trình đều lưu dữ liệu trên local, sau đó 1 tiến trình ngầm sẽ thực hiện việc đồng bộ một cách tự động.

Trong khuôn khổ bài viết này tôi sẽ hướng dẫn các bạn nâng cấp chương trình ParseServiceDemo để support việc đồng bộ dữ liệu local.

Việc đăng ký và tạo ứng dụng trên Parse Service các bạn tham khảo ở link trên.

2.1. Tạo MGParseDemo app

Mở Xcode tạo 1 ứng dụng Single View Application. Đặt tên ứng dụng là MGParseDemo, ngôn ngữ Swift, bỏ chọn Use Core Data.

New Project

2.2. Cấu hình Parse SDK

Vào mục download trên Parse để download SDK mới nhất dành cho iOS, (hiện tại là v1.8.1)

Kéo thả Parse.frameworkBolts.framework bạn vừa download vào project trên Xcode. Tích chọn Copy items if needed

Chọn Targets > ParseDemo > Build Phases tab.

Thêm các library sau vào mục Link Binary With Libraries:

  • AudioToolbox.framework
  • CFNetwork.framework
  • CoreGraphics.framework
  • CoreLocation.framework
  • MobileCoreServices.framework
  • QuartzCore.framework
  • Security.framework
  • StoreKit.framework
  • SystemConfiguration.framework
  • libz.dylib
  • libsqlite3.dylib
  • Accounts.framework
  • Social.framework

Framework

Cập nhật file AppDelegate như sau, thay ApppicationIdclientKey theo app của bạn.

import UIKit
import Parse
import Bolts

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        // Override point for customization after application launch.

        Parse.enableLocalDatastore()

        // Initialize Parse.
        Parse.setApplicationId("e3wOeG3tvmL5LryZ6w1imB66WXci7J28SLbX1ud5",
            clientKey: "TpvMTzgKm1n9OeCVVJUHnQ53LXJNkLYmlx0cl3LZ")

        // [Optional] Track statistics around application opens.
        PFAnalytics.trackAppOpenedWithLaunchOptions(launchOptions)

        return true
    }
}

2.3. Thêm Core Data Model

Thêm Core Data Model vào dự án (New File... > Core Data > Data Model)

Tạo Product Entity như sau:

Product Entity

Tạo NSManagedObject class cho Product Entity (New File... > Core Data > NSManagedObject subclass)

@interface Product : NSManagedObject

@property (nonatomic, retain) NSString * id;
@property (nonatomic, retain) NSDate * creation_date;
@property (nonatomic, retain) NSDate * modification_date;
@property (nonatomic, retain) NSString * name;
@property (nonatomic, retain) NSNumber * price;

@end

Chương trình sẽ hỏi thêm file bridging header MGParseDemo-Bridging-Header.h

Sau khi đồng ý, ta thêm dòng import file:

#import "Product.h"

2.4. Thêm thư viện MagicalRecord

Ta sử dụng cocoapod để thêm thư viện.

Thêm dòng import vào file bridging header

#import "MagicalRecord.h"

Thêm dòng sau vào hàm didFinishLaunchingWithOptions của AppDelegate

MagicalRecord.setupCoreDataStack()

và hàm applicationWillTerminate

MagicalRecord.cleanUp()

2.5. Tạo Product DTO

Ta sẽ sử dụng DTO để tránh dùng trực tiếp Core Data Entity

class ProductDto: NSObject {
    var id: String       = NSUUID().UUIDString
    var creationDate     = NSDate()
    var modificationDate = NSDate()
    var name             = ""
    var price: Float     = 0
}

2.6. Tạo Mapper class

Mapper sẽ có nhiệm vụ map dữ liệu giữa Core Data Entity và DTO object.

class Mapper: NSObject {
    class func mapFromProduct(product: Product, toProductDto productDto: ProductDto) {
        productDto.id = product.id
        productDto.creationDate = product.creation_date
        productDto.modificationDate = product.modification_date
        productDto.name = product.name
        productDto.price = product.price.floatValue
    }

    class func mapFromProductDto(productDto: ProductDto, toProduct product: Product) {
        product.id = productDto.id
        product.creation_date = productDto.creationDate
        product.modification_date = productDto.modificationDate
        product.name = productDto.name
        product.price = productDto.price
    }

    class func productDtoFromProduct(product: Product) -> ProductDto {
        let productDto = ProductDto()
        Mapper.mapFromProduct(product, toProductDto: productDto)
        return productDto
    }
}

2.7. Tạo Product Repository

Thêm class ProductRepository, sử dụng để thêm sửa xóa và lấy danh sách Product từ Core Data database.

class ProductRepository: NSObject {
    func addProduct(productDto: ProductDto) {
        MagicalRecord.saveWithBlockAndWait { (context) -> Void in
            let product = Product.MR_createEntityInContext(context)
            Mapper.mapFromProductDto(productDto, toProduct: product)
        }
    }

    func updateProduct(productDto: ProductDto) {
        MagicalRecord.saveWithBlockAndWait { (context) -> Void in
            let predicate = NSPredicate(format: "id = '(productDto.id)'")
            let product = Product.MR_findFirstWithPredicate(predicate, inContext: context)
            if product != nil {
                Mapper.mapFromProductDto(productDto, toProduct: product)
            }
        }
    }

    func deleteProductById(id: String) {
        MagicalRecord.saveWithBlockAndWait { (context) -> Void in
            let predicate = NSPredicate(format: "id = '(id)'")
            let product = Product.MR_findFirstWithPredicate(predicate, inContext: context)
            if product != nil {
                Product.MR_deleteAllMatchingPredicate(predicate, inContext: context)
            }
        }
    }

    func count() -> Int {
        let context = NSManagedObjectContext.MR_context()
        return Int(Product.MR_countOfEntitiesWithContext(context))
    }

    func getAllProducts() -> [ProductDto] {
        let context = NSManagedObjectContext.MR_context()
        let products = Product.MR_findAllInContext(context) as! [Product]
        var productDtos = [ProductDto]()

        for product in products {
            let productDto = Mapper.productDtoFromProduct(product)
            productDtos.append(productDto)
        }
        return productDtos
    }

    func mostRecentUpdatedDate() -> NSDate? {
        let context = NSManagedObjectContext.MR_context()
        if let products = Product.MR_findAllSortedBy("modification_date", ascending: false) as? [Product] {
            if products.count > 0 {
                return products[0].modification_date
            }
        }

        return nil
    }

    func getProductsUpdatedAfterDate(date: NSDate) -> [ProductDto] {
        let context = NSManagedObjectContext.MR_context()
        let predicate = NSPredicate(format: "modification_date > %@", date)
        let products = Product.MR_findAllSortedBy("modification_date", ascending: true, withPredicate: predicate) as! [Product]
        var productDtos = [ProductDto]()

        for product in products {
            let productDto = Mapper.productDtoFromProduct(product)
            productDtos.append(productDto)
        }
        return productDtos
    }
}

2.8. Tạo Product Parse Client

Thêm class ProductParseClient, sử dụng để thêm sửa xóa và lấy danh sách Product từ Parse Service database.

import UIKit
import Parse

class ProductParseClient: NSObject {
    func addProduct(product: ProductDto) {
        var obj = PFObject(className: "Product")
        obj.setObject(product.id, forKey: "id")
        obj.setObject(product.creationDate, forKey: "creationDate")
        obj.setObject(product.modificationDate, forKey: "modificationDate")
        obj.setObject(product.name, forKey: "name")
        obj.setObject(product.price, forKey: "price")
        obj.save()
    }

    func updateProduct(product: ProductDto) {
        var query = PFQuery(className: "Product")
        query.whereKey("id", equalTo: product.id)
        if let objects = query.findObjects() {
            for obj in objects {
                obj.setObject(product.modificationDate, forKey: "modificationDate")
                obj.setObject(product.name, forKey: "name")
                obj.setObject(product.price, forKey: "price")
                obj.save()
            }
        }
    }

    func deleteProduct(productID: String) {
        var query = PFQuery(className: "Product")
        query.whereKey("id", equalTo: productID)
        if let objects = query.findObjects() {
            for obj in objects {
                obj.delete()
            }
        }
    }

    func count() -> Int {
        var query = PFQuery(className: "Product")
        return query.countObjects()
    }

    func getAllProducts() -> [ProductDto] {
        var products = [ProductDto]()
        var query = PFQuery(className: "Product")

        if let objects = query.findObjects() {
            for obj in objects {
                var product = ProductDto()
                product.id = obj.objectForKey("id") as! String
                product.name = obj.objectForKey("name") as! String
                product.price = obj.objectForKey("price") as! Float
                product.creationDate = obj.objectForKey("creationDate") as! NSDate
                product.modificationDate = obj.objectForKey("modificationDate") as! NSDate
                products.append(product)
            }
        }

        return products
    }

    func mostRecentUpdatedDate() -> NSDate? {
        var query = PFQuery(className: "Product")
        query.orderByDescending("modificationDate")
        if let obj = query.getFirstObject() {
            return obj.objectForKey("modificationDate") as? NSDate
        }
        return nil
    }

    func getProductsUpdatedAfterDate(date: NSDate) -> [ProductDto] {
        var products = [ProductDto]()
        var query = PFQuery(className: "Product")
        query.whereKey("modificationDate", greaterThan: date)

        if let objects = query.findObjects() {
            for obj in objects {
                var product = ProductDto()
                product.id = obj.objectForKey("id") as! String
                product.name = obj.objectForKey("name") as! String
                product.price = obj.objectForKey("price") as! Float
                product.creationDate = obj.objectForKey("creationDate") as! NSDate
                product.modificationDate = obj.objectForKey("modificationDate") as! NSDate
                products.append(product)
            }
        }

        return products
    }
}

2.9. Tạo Product Service

ProductService sử dụng ProductRepository để thêm sửa xóa và lấy danh sách Product từ Core Data database.

class ProductService: NSObject {
    let productRepository = ProductRepository()

    func addProduct(productDto: ProductDto) {
        productRepository.addProduct(productDto)
    }

    func updateProduct(productDto: ProductDto) {
        productRepository.updateProduct(productDto)
    }

    func deleteProductById(id: String) {
        productRepository.deleteProductById(id)
    }

    func getAllProducts() -> [ProductDto] {
        return productRepository.getAllProducts()
    }

    func mostRecentUpdatedDate() -> NSDate? {
        return productRepository.mostRecentUpdatedDate()
    }

    func getProductsUpdatedAfterDate(date: NSDate) -> [ProductDto] {
        return productRepository.getProductsUpdatedAfterDate(date)
    }
}

2.10. Tạo Sync Service

SyncService có nhiệm vụ đồng bộ dữ liệu giữa Core Data database (local) và Parse Service database.

Hàm sycn sẽ lấy thời gian cập nhật mới nhất của local database và parse database và so sánh 2 thời gian này, tùy từng trường hợp mà sẽ update dữ liệu local và dữ liệu parse.

class SyncService: NSObject {
    let productRepository = ProductRepository()
    let productParseClient = ProductParseClient()

    func sync() {
        let localUpdatedDate: NSDate? = productRepository.mostRecentUpdatedDate()
        let parseUpdatedDate: NSDate? = productParseClient.mostRecentUpdatedDate()

        if localUpdatedDate == nil {
            if parseUpdatedDate != nil {
                let products = productParseClient.getAllProducts()
                for product in products {
                    productRepository.addProduct(product)
                }
            }
        }
        else {
            if parseUpdatedDate != nil {
                if localUpdatedDate!.compare(parseUpdatedDate!) == NSComparisonResult.OrderedDescending {  // localUpdatedDate > parseUpdatedDate
                    let products = productRepository.getProductsUpdatedAfterDate(parseUpdatedDate!)
                    for product in products {
                        productParseClient.addProduct(product)
                    }
                }
                else {
                    let products = productParseClient.getProductsUpdatedAfterDate(parseUpdatedDate!)
                    for product in products {
                        productRepository.addProduct(product)
                    }
                }
            }
            else {
                let products = productRepository.getAllProducts()
                for product in products {
                    productParseClient.addProduct(product)
                }
            }
        }
    }
}

(Kết thúc phần 1)

Các bạn có thể tham khảo source code tại đây

0