iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど
今回は、ヒレガス本の日本語版初版のコントロールの章。サンプルは、Document-based Applicationだ。
以前の開発環境との大きな違いは、NSViewControllerがiOSのUIViewControllerに近づいたのとStoryboardだ。最新の環境でもStorybardを使用しないで新規プロジェクトを生成すると、以前に近いのだが、Sgtoryboardを使用すると大きく異なる。
macOSではInterfaceBuilderは汎用的で、nibのFile's Ownerに設定されるクラスは多様だ。iOSではUIViewControllerなど、File's Ownerに設定されるクラスの種類は少なように感じるが、これがmacOSアプリケーションの開発を難しく感じてしまう原因の一つではないかと思っている。
Storyboardを使わない場合、Document.xibが生成され、ウィンドウに配置されたコントローラに対するOutletとActionはDocumentクラスに用意されることになる。
class Document: NSDocument {
@IBOutlet public weak var deleteButton: NSButton!
@IBOutlet public weak var nextButton: NSButton!
@IBOutlet public weak var previousButton: NSButton!
@IBOutlet public weak var nameField: NSTextField!
@IBOutlet public weak var raiseField: NSTextField!
@IBOutlet public weak var box: NSBox!
@IBAction func nextEmployee(sender: AnyObject) {
}
@IBAction func previousEmployee(sender: AnyObject) {
}
@IBAction func deleteEmployee(sender: AnyObject) {
}
@IBAction func newEmployee(sender: AnyObject) {
}
}
Storyboardを使った場合、OutletはViewControllerで持つのが自然だろう。Actionについては、ViewControllerでも、DocumentでもどちらでもOKだが、今回は、ViewControllerで持ち、ViewControllerがDocumentを操作し、Documentの更新を通知でViewControlelrで受けるようにした。
import Cocoa
class ViewController: NSViewController {
@IBOutlet public weak var deleteButton: NSButton!
@IBOutlet public weak var nextButton: NSButton!
@IBOutlet public weak var previousButton: NSButton!
@IBOutlet public weak var nameField: NSTextField!
@IBOutlet public weak var raiseField: NSTextField!
@IBOutlet public weak var box: NSBox!
var myDocument: Document?
override func awakeFromNib() {
self.view.window?.initialFirstResponder = self.nameField
}
override func viewDidLoad() {
super.viewDidLoad()
}
override var representedObject: Any? {
didSet {
}
}
override func viewWillAppear() {
super.viewWillAppear()
self.myDocument = self.view.window?.windowController?.document as? Document
NotificationCenter.default.addObserver(self,
selector: #selector(type(of: self).updateUI(notification:)),
name: Document.updateKey,
object: nil)
}
override func viewWillDisappear() {
NotificationCenter.default.removeObserver(self, name: Document.updateKey, object: nil)
super.viewWillDisappear()
}
@IBAction func nextEmployee(sender: AnyObject) {
self.myDocument?.nextEmployee(personName: nameField.stringValue, expectedRaise: raiseField.floatValue)
}
@IBAction func previousEmployee(sender: AnyObject) {
self.myDocument?.previousEmployee(personName: nameField.stringValue, expectedRaise: raiseField.floatValue)
}
@IBAction func deleteEmployee(sender: AnyObject) {
self.myDocument?.deleteEmployee()
}
@IBAction func newEmployee(sender: AnyObject) {
self.myDocument?.newEmployee(personName: nameField.stringValue, expectedRaise: raiseField.floatValue)
}
@objc private func updateUI(notification: Notification) {
let recordText = "Record \((self.myDocument?.currentIndex)! + 1) of \(self.myDocument?.employees.count)"
let currentEmployee = self.myDocument?.employees[(self.myDocument?.currentIndex)!]
nameField.stringValue = (currentEmployee?.personName)!
raiseField.floatValue = (currentEmployee?.expectedRaise)!
box.title = recordText
previousButton.isEnabled = ((self.myDocument?.currentIndex)! > 0)
nextButton.isEnabled = ((self.myDocument?.currentIndex)! < ((self.myDocument?.employees.count)! - 1))
deleteButton.isEnabled = ((self.myDocument?.employees.count)! > 1)
}
}
import Cocoa
class Person {
public var personName: String = "New Employee"
public var expectedRaise: Float = 0.0
}
class Document: NSDocument {
public static let updateKey = NSNotification.Name("updateUI")
public var employees = [Person]()
public var currentIndex: Int = 0
override init() {
super.init()
createNewEmployee()
}
override class func autosavesInPlace() -> Bool {
return true
}
override func makeWindowControllers() {
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController(withIdentifier: "Document Window Controller") as! NSWindowController
self.addWindowController(windowController)
}
override func data(ofType typeName: String) throws -> Data {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
override func read(from data: Data, ofType typeName: String) throws {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
public func nextEmployee(personName: String, expectedRaise: Float) {
updateEmployee(personName: personName, expectedRaise: expectedRaise)
currentIndex += 1
updateUI()
}
public func previousEmployee(personName: String, expectedRaise: Float) {
updateEmployee(personName: personName, expectedRaise: expectedRaise)
currentIndex -= 1
updateUI()
}
public func deleteEmployee() {
employees.remove(at: currentIndex)
if currentIndex != 0 {
currentIndex -= 1
}
updateUI()
}
public func newEmployee(personName: String, expectedRaise: Float) {
updateEmployee(personName: personName, expectedRaise: expectedRaise)
createNewEmployee()
updateUI()
}
private func createNewEmployee() {
let newEmployee = Person()
employees.append(newEmployee)
currentIndex = employees.count - 1
}
private func updateEmployee(personName: String, expectedRaise: Float) {
var currentEmployee = employees[currentIndex]
currentEmployee.personName = personName
currentEmployee.expectedRaise = expectedRaise
}
private func updateUI() {
NotificationCenter.default.post(name: Document.updateKey, object: nil, userInfo: nil)
}
}
次に問題になったのは、NSWindowのinitialFirstResponder。Storyboardを使わない場合、WindowのOutletをViewにつなぐことができるが、Storyboardを使う場合は、別Sceneなので繋げられない。
今回は、awakeFromNib()で、コードで設定するようにした。
class ViewController: NSViewController {¥
override func awakeFromNib() {
self.view.window?.initialFirstResponder = self.nameField
}
}
今回は、Cocoa勉強会で知り合った方々の情報で課題が解決できた。ありがとう。