Mac OS X Application
Bài đăng này đã không được cập nhật trong 3 năm
Mac Application
1. Giới thiệu
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. Ứng dụng MGMacProductDemo
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
3. Kết luận
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
All rights reserved