Đồng bộ dữ liệu Core Data với Parse Service (Phần 1 + 2)
(Ghi chú: gộp phần 1 và 2, update Swift 2.0, update product entity & service class) Ở 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à ...
(Ghi chú: gộp phần 1 và 2, update Swift 2.0, update product entity & service class)
Ở 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.
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.framework và Bolts.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
Cập nhật file AppDelegate như sau, thay ApppicationId và clientKey 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:
Tạo NSManagedObject class cho Product Entity (New File... > Core Data > NSManagedObject subclass)
#import "Product.h" NS_ASSUME_NONNULL_BEGIN @interface Product (CoreDataProperties) @property (nullable, nonatomic, retain) NSDate *creation_date; @property (nullable, nonatomic, retain) NSString *id; @property (nullable, nonatomic, retain) NSDate *modification_date; @property (nullable, nonatomic, retain) NSString *name; @property (nullable, nonatomic, retain) NSNumber *price; @property (nullable, nonatomic, retain) NSNumber *status; @end NS_ASSUME_NONNULL_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
import UIKit enum ProductStatus: Int { case New case Updated case Deleted } class ProductDto: NSObject { var id: String = NSUUID().UUIDString var creationDate = NSDate() var modificationDate = NSDate() var name = "" var price: Float = 0 var status = ProductStatus.New }
2.6. Tạo Mapper class
Mapper sẽ có nhiệm vụ map dữ liệu giữa Core Data Entity và DTO object.
import UIKit 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 productDto.status = ProductStatus(rawValue: product.status!.integerValue)! } 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 product.status = productDto.status.rawValue } 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.
import UIKit 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 isProductExistedForId(id: String) -> Bool { let predicate = NSPredicate(format: "id = '(id)'") let product = Product.MR_findFirstWithPredicate(predicate) if product != nil { return true } return false } func count() -> Int { return Int(Product.MR_countOfEntities()) } func getAllProducts(includeDeletedProduct: Bool = false) -> [ProductDto] { let predicate = NSPredicate(format: "status != '(ProductStatus.Deleted.rawValue)'") var products: [Product]! if includeDeletedProduct { products = Product.MR_findAll() as! [Product] } else { products = Product.MR_findAllWithPredicate(predicate) as! [Product] } var productDtos = [ProductDto]() for product in products { let productDto = Mapper.productDtoFromProduct(product) productDtos.append(productDto) } return productDtos } func mostRecentUpdatedDate() -> NSDate? { 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 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) { let 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.setObject(product.status.rawValue, forKey: "status") do { try obj.save() } catch { print("Save error!") } } func updateProduct(product: ProductDto) { let query = PFQuery(className: "Product") query.whereKey("id", equalTo: product.id) do { let objects = try 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.setObject(product.status.rawValue, forKey: "status") try obj.save() } } catch { print("Query error!") } } func isProductExistedForId(id: String) -> Bool { let query = PFQuery(className: "Product") query.whereKey("id", equalTo: id) do { let objects = try query.findObjects() return objects.count > 0 } catch { print("Query error!") } return false } func deleteProduct(productID: String) { let query = PFQuery(className: "Product") query.whereKey("id", equalTo: productID) do { let objects = try query.findObjects() for obj in objects { try obj.delete() } } catch { print("Delete object error!") } } func count() -> Int { let query = PFQuery(className: "Product") var count = -1 var error: NSError? count = query.countObjects(&error) return count } func getAllProducts() -> [ProductDto] { var products = [ProductDto]() let query = PFQuery(className: "Product") do { let objects = try query.findObjects() products += productDtosFromPFObjects(objects) } catch { print("Get products error!") } return products } private func productDtosFromPFObjects(objects: [PFObject]) -> [ProductDto] { var products = [ProductDto]() for obj in objects { let 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 product.status = ProductStatus(rawValue: obj.objectForKey("status") as! Int)! products.append(product) } return products } func mostRecentUpdatedDate() -> NSDate? { let query = PFQuery(className: "Product") query.orderByDescending("modificationDate") do { let obj = try query.getFirstObject() return obj.objectForKey("modificationDate") as? NSDate } catch { print("Get most recent updated date error!") } return nil } func getProductsUpdatedAfterDate(date: NSDate) -> [ProductDto] { var products = [ProductDto]() let query = PFQuery(className: "Product") query.whereKey("modificationDate", greaterThan: date) do { let objects = try query.findObjects() products += productDtosFromPFObjects(objects) } catch { print("Get products error!") } 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.
Chú ý hàm deleteProduct, chúng ta không thực sự xóa product khỏi database mà chỉ update thuộc tính status của product thành Deleted, việc này sẽ giúp cho việc đồng bộ dữ liệu được đơn giản hơn.
import UIKit class ProductService: NSObject { let productRepository = ProductRepository() func addProduct(productDto: ProductDto) { productRepository.addProduct(productDto) } func updateProduct(productDto: ProductDto) { productDto.modificationDate = NSDate() productDto.status = ProductStatus.Updated productRepository.updateProduct(productDto) } func addOrUpdateProduct(productDto: ProductDto) { if isProductExistedForId(productDto.id) { productRepository.updateProduct(productDto) } else { productRepository.addProduct(productDto) } } func deleteProduct(productDto: ProductDto) { productDto.modificationDate = NSDate() productDto.status = ProductStatus.Deleted productRepository.updateProduct(productDto) } func isProductExistedForId(id: String) -> Bool { return productRepository.isProductExistedForId(id) } func getAllProducts(includeDeletedP