Mac OS X Application
Gần đây, chính sách mới của Apple đã gộp iOS và Mac developer program vào làm một, chúng ta chỉ phải trả 99$$năm cho cả 2 program, việc này khuyến khích các lập trình viên iOS viết thêm phiên bản Mac cho các app iOS của họ, hay viết các app Mac mới hoàn toàn, giúp gia tăng số lượng ứng dụng cho ...
Gần đây, chính sách mới của Apple đã gộp iOS và Mac developer program vào làm một, chúng ta chỉ phải trả 99$$năm cho cả 2 program, việc này khuyến khích các lập trình viên iOS viết thêm phiên bản Mac cho các app iOS của họ, hay viết các app Mac mới hoàn toàn, giúp gia tăng số lượng ứng dụng cho Mac, nhằm cạnh tranh với Windows Market.
Có một thuận lợi lớn cho các lập trình viên iOS khi muốn viết app cho Mac đó là việc tạo một app cho Mac khá tương đồng với iOS, bạn vẫn dùng có thể dùng Objective-C hay Swift để code và phần lớn các thư viện đều có phiên bản cho Mac. Công việc khó khăn nhất với chúng ta đấy là phần thư viện UI của Mac tương đối khác với iOS.
Trong bài viết này tôi sẽ hướng dẫn các bạn viết một ứng dụng Mac đơn giản dùng để quản lý sản phẩm.
2.1. Tạo project
Để bắt đầu chúng ta tạo một project mới theo template OS X > Application > Cocoa Application
Ta điền các thông tin của project tương tự dưới đây, ngôn ngữ Swift và sử dụng Storyboards
Sau đó chúng ta có thể chạy thử ứng dụng qua menu Product > Run (Command + R), ta sẽ thấy 1 cửa sổ chương trình trắng tinh như hình dưới.
2.2. Core Data
Ứng dụng của chúng ta sẽ sử dụng database để lưu thông tin sản phẩm, để thêm database, ta chọn New File > OS X > Core Data > Data Model
Sau đó ta thêm ProductEntity như sau:
Tạo NSManagedObject subclass cho model vừa tạo bằng cách chuột phải vào file Model chọn New File > OS X > Core Data > NSManagedObject subclass
Ta cần include file ProductEntity.h vào file MGMacProductDemo-Bridging-Header.h (file này sẽ được Xcode thêm vào khi ta thêm file Objective-C vào dự án)
#import "ProductEntity.h"
MagicalRecord
Ta sẽ dùng thư viện MagicalRecord để tương tác với Code Data. Import vào dự án thông qua CocoaPod
pod 'MagicalRecord', '~> 2.3'
Thêm dòng import vào file MGMacProductDemo-Bridging-Header.h
#import "MagicalRecord.h"
Để cấu hình MagicalRecord chúng ta cần 2 dòng lệnh, 1 dòng dùng để khởi tạo và 1 dòng dùng để dọn dẹp sau khi đóng ứng dụng. Dòng khởi tạo chúng ta sẽ viết tại hàm windowDidLoad() của MainWindowController sẽ trình bày ở dưới, dòng dọn dẹp chúng ta viết ở applicationWillTerminate(_:) của AppDelegate
class AppDelegate: NSObject, NSApplicationDelegate func applicationDidFinishLaunching(aNotification: NSNotification) { // Insert code here to initialize your application } func applicationShouldTerminateAfterLastWindowClosed(sender: NSApplication) -> Bool { return true } func applicationWillTerminate(aNotification: NSNotification) { MagicalRecord.cleanUp() } }
2.3. Repository, Service
Để không sử dụng product model trực tiếp, ta sẽ dùng class Product và map với ProductEntity thông qua class Mapper.
Product
class Product { var id = "" var creationDate = NSDate() var modificationDate = NSDate() var name = "" var price = 0.0 }
Mapper
class Mapper { var id = NSUUID().UUIDString var creationDate = NSDate() var modificationDate = NSDate() var name = "" var price = 0.0 class func mapProduct(product: Product, fromProductEntity productEntity: ProductEntity) { product.id = productEntity.id ?? "" product.creationDate = productEntity.creationDate ?? NSDate() product.modificationDate = productEntity.modificationDate ?? NSDate() product.name = productEntity.name ?? "" product.price = productEntity.price?.doubleValue ?? 0 } class func mapProductEntity(productEntity: ProductEntity, fromProduct product: Product) { productEntity.id = product.id productEntity.creationDate = product.creationDate productEntity.modificationDate = product.modificationDate productEntity.name = product.name productEntity.price = product.price } class func productFromProductEntity(productEntity: ProductEntity) -> Product { let product = Product() mapProduct(product, fromProductEntity: productEntity) return product } }
ProductRepository
ProductRepository dùng để tương tác với database, thực hiện các chức năng lấy danh sách product, thêm, sửa, xóa product.
class ProductRepository: NSObject { func getAll() -> [Product] { var products = [Product]() if let productEntities = ProductEntity.MR_findAllSortedBy("modificationDate", ascending: false) as? [ProductEntity] { for entity in productEntities { products.append(Mapper.productFromProductEntity(entity)) } } return products } func addProduct(product: Product) { MagicalRecord.saveWithBlockAndWait { (context) -> Void in if let entity = ProductEntity.MR_createEntityInContext(context) { Mapper.mapProductEntity(entity, fromProduct: product) } } } func updateProduct(product: Product) { MagicalRecord.saveWithBlockAndWait { (context) -> Void in let predicate = NSPredicate(format: "id = '(product.id)'") if let entity = ProductEntity.MR_findFirstWithPredicate(predicate, inContext: context) { entity.modificationDate = NSDate() Mapper.mapProductEntity(entity, fromProduct: product) } } } func deleteProduct(product: Product) { MagicalRecord.saveWithBlockAndWait { (context) -> Void in let predicate = NSPredicate(format: "id = '(product.id)'") ProductEntity.MR_deleteAllMatchingPredicate(predicate, inContext: context) } } }
ProductService
Để tăng tính lỏng cho chương trình, chúng ta sẽ dùng ProductService implement ProductServiceProtocol
protocol ProductServiceProtocol { func getAll() -> [Product] func addProduct(product: Product) func updateProduct(product: Product) func deleteProduct(product: Product) }
class ProductService: NSObject, ProductServiceProtocol { let productRepository = ProductRepository() func getAll() -> [Product] { return productRepository.getAll() } func addProduct(product: Product) { productRepository.addProduct(product) } func updateProduct(product: Product) { productRepository.updateProduct(product) } func deleteProduct(product: Product) { productRepository.deleteProduct(product) } }
2.4. Xây dựng UI và các controller
Về giao diện của chương trình, chúng ta sẽ cần 1 màn quản lý danh sách product, 1 màn cho phép thêm và sửa thông tin của từng product, chi tiết như sau:
Bạn sẽ cần phải thêm toolbar vào trong MainWindowController, toolbar có 3 nút Add, Delete, Edit:
Ở MainViewController chúng ta cần thêm 1 NSTableView có 2 cột Name và Price. MainViewController có 1 segue dạng Sheet sang ProductViewController.
Ghi chú: segue có 1 số dạng cũng khá tương đồng với iOS bao gồm:
- Show: hiện view controller ở dạng cửa sổ mới
- Modal: hiện view controller ở dạng cửa sổ modal (nằm trên cùng và không cho phép thao tác với các cửa sổ khác)
- Sheet: hiện view controller ở 1 sheet - 1 dạng view trượt xuống từ thanh tiêu đề của app
- Popup: hiện view controller ở trong 1 một cửa sổ dạng bong bóng
- Custom: loại tùy chỉnh
ProductViewController gồm 2 NSTextField cho việc nhập Name và Price và 2 NSButton cho việc xác nhận và hủy bỏ.
Trong OSX, mỗi đối tượng của class NSWindowController quản lý một window, bao gồm:
- Tải và hiện thị window
- Đóng window khi cần
- Thay đổi tiêu đề cửa sổ
- Lưu trữ frame (kích thước, vị trí) của cửa sổ trong database mặc định
Trong iOS không có khái niệm window controller do chỉ có duy nhất 1 window cho 1 ứng dụng.
NSViewController trong Mac OSX cũng tương đồng với UIViewController trong iOS, NSViewController quản lý các view và có các hàm phản ánh vòng đời của view:
- viewDidLoad
- viewWillAppear
- viewDidAppear
- viewWillDisappear
- viewDidDisappear
Để kéo @IBAction, @IBOutlet chúng ta cũng thực hiện tương tự như iOS thông qua Assistant editor:
MainWindowController
Không giống như iOS, hàm windowDidLoad() của MainWindowController sẽ chạy trước applicationDidFinishLaunching(_:) của AppDelegate nên ta sẽ đặt các đoạn code khởi tạo ở đây.
Ta sẽ khởi tạo MagicalRecord qua dòng lệnh:
MagicalRecord.setupAutoMigratingCoreDataStack()
MainWindowController sẽ xử lý sự kiện người dùng thao tác với toolbar và đưa việc xử lý này cho MainViewController qua MainWindowControllerDelegate
protocol MainWindowControllerDelegate: class { func mainWindowControllerClickedAddButton() func mainWindowControllerClickedDeleteButton() func mainWindowControllerClickedEditButton() } class MainWindowController: NSWindowController { weak var delegate: MainWindowControllerDelegate? override func windowDidLoad() { super.windowDidLoad() MagicalRecord.setupAutoMigratingCoreDataStack() window?.titleVisibility = NSWindowTitleVisibility.Hidden let mainViewController = self.contentViewController as? MainViewController mainViewController?.window = self.window self.delegate = mainViewController } @IBAction func onAddButtonClicked(sender: NSToolbarItem) { delegate?.mainWindowControllerClickedAddButton() } @IBAction func onDeleteButtonClicked(sender: NSToolbarItem) { delegate?.mainWindowControllerClickedDeleteButton() } @IBAction func onEditButtonClicked(sender: NSToolbarItem) { delegate?.mainWindowControllerClickedEditButton() } }
MainViewController
MainViewController có nhiệm vụ hiển thị danh sách product thông qua một table view. Cũng giống như iOS việc này được thực hiện thông qua việc implement NSTableViewDataSource, và NSTableViewDelegate của NSTableView.
MainViewController còn có nhiệm vụ thực hiện các chức năng thêm, sửa, xóa product. Trong đó việc thêm và sửa sẽ được thực hiện thông qua ProductViewController, chúng ta gọi ProductViewController thông qua 1 segue có kiểu Sheet được thiết lập tại storyboard.
Tại chức năng xóa, chúng ta cũng cho phép người dùng xác nhận việc xóa thông qua việc hiển thị một NSAlert theo kiểu Sheet thông qua hàm beginSheetModalForWindow(_:completionHandler:) của alert.
Mỗi khi thực hiện xong 1 tác vụ, chúng ta sẽ tải lại danh sách product thông qua hàm fetchProduct()
class MainViewController: NSViewController { @IBOutlet weak var tableView: NSTableView! weak var window: NSWindow! var productService: ProductServiceProtocol = ProductService() var products: [Product]! override func viewDidLoad() { super.viewDidLoad() // Do view setup here. tableView.setDataSource(self) tableView.setDelegate(self) } override func viewDidAppear() { super.viewDidAppear() fetchProduct() } private func fetchProduct() { products = productService.getAll() tableView.reloadData() } override func prepareForSegue(segue: NSStoryboardSegue, sender: AnyObject?) { if segue.identifier == "sheetProduct" { let controller = segue.destinationController as! ProductViewController controller.delegate = self controller.product = sender as? Product } } } extension MainViewController: NSTableViewDataSource { func numberOfRowsInTableView(tableView: NSTableView) -> Int { return products?.count ?? 0 } } extension MainViewController: NSTableViewDelegate { func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? { var view: NSTableCellView? let product = products[row] if tableColumn?.identifier == "NameColumn" { view = tableView.makeViewWithIdentifier("NameCell", owner: self) as? NSTableCellView view?.textField?.stringValue = product.name } else if tableColumn?.identifier == "PriceColumn" { view = tableView.makeViewWithIdentifier("PriceCell", owner: self) as? NSTableCellView view?.textField?.stringValue = "(product.price)" } return view } } extension MainViewController: MainWindowControllerDelegate { func mainWindowControllerClickedAddButton() { self.performSegueWithIdentifier("sheetProduct", sender: nil) } func mainWindowControllerClickedDeleteButton() { let selectedRow = tableView.selectedRow if selectedRow >= 0 && selectedRow < products.count { let product = products[selectedRow] let alert = NSAlert() alert.messageText = "Are you sure you want to delete product: " + product.name alert.informativeText = "You can't undo this action." alert.addButtonWithTitle("Delete") alert.addButtonWithTitle("Cancel") alert.alertStyle = NSAlertStyle.WarningAlertStyle alert.beginSheetModalForWindow(self.window!, completionHandler: { (response) -> Void in if response == NSAlertFirstButtonReturn { self.productService.deleteProduct(product) self.fetchProduct() } }) } } func mainWindowControllerClickedEditButton() { let selectedRow = tableView.selectedRow if selectedRow >= 0 && selectedRow < products.count { let product = products[selectedRow] self.performSegueWithIdentifier("sheetProduct", sender: product) } } } extension MainViewController: ProductViewControllerDelegate { func productViewControllerDone(sender: ProductViewController) { let product = sender.product product.modificationDate = NSDate() if product.id == "" { product.id = NSUUID().UUIDString product.creationDate = NSDate() productService.addProduct(product) } else { productService.updateProduct(product) } fetchProduct() } }
ProductViewController
ProductViewController cho phép người dùng thêm và sửa thông tin sản phẩm bao gồm tên và giá sản phẩm. Việc người dùng nhấn xác nhận hay hủy bỏ sẽ được truyền về MainViewController thông qua ProductViewControllerDelegate
Sau đó căn cứ vào id của sản phẩm, chúng ta sẽ biết là sản phẩm được tạo mới hay chỉ là cập nhật thông tin để tiến hành xử lý tương ứng tại MainViewController
protocol ProductViewControllerDelegate: class { func productViewControllerDone(sender: ProductViewController) } class ProductViewController: NSViewController { @IBOutlet weak var nameTextField: NSTextField! @IBOutlet weak var priceTextField: NSTextField! var product: Product! weak var delegate: ProductViewControllerDelegate? override func viewDidLoad() { super.viewDidLoad() if product != nil { nameTextField.stringValue = product.name priceTextField.stringValue = String(product.price) } } @IBAction func onOKButtonClicked(sender: NSButton) { if product == nil { product = Product() } product.name = nameTextField.stringValue product.price = (priceTextField.stringValue as NSString).doubleValue delegate?.productViewControllerDone(self) self.dismissController(nil) } @IBAction func onCancelButtonClicked(sender: NSButton) { self.dismissController(nil) } }
2.5. Chạy demo
Danh sách sản phẩm
Thêm sản phẩm
Xóa sản phẩm
Như vậy chúng ta đã hoàn thành một phần mềm đơn giản trên Mac OS X, hi vọng bài viết này sẽ giúp các bạn có thêm cảm hứng và ý tưởng để cho ra ứng dụng của riêng mình trên Mac App Store.
Các bạn có thể download source code tại đây