توسعه یک اپلیکیشن MacOS ساده — از صفر تا صد
در این مقاله قصد داریم با روش توسعه یک اپلیکیشن MacOS ساده آشنا شویم. این اپلیکیشن یک ساعت در نوار وضعیت سیستم عامل خواهد بود که امکان تنظیم یادآور دارد و کاربر میتواند نوتیفیکیشنها را در آن زمانبندی کند. در این فرایند با روش جلوگیری از باز شدن یک پنجره برای چندین بار، تنظیم نوتیفیکیشن و موارد دیگر آشنا خواهیم شد.
پیشنیازها برای توسعه اپلیکیشن MacOS
برای مطالعه این راهنما، برخی موارد نیاز هستند که در ادامه فهرست شدهاند:
- دانش مقدماتی از زبان برنامهنویسی سوئیفت.
- یک سیستم مک که روی آن Xcode نصب شده است.
- نسخه Xcode باید 10.2 یا بالاتر باشد. گرچه نسخههای قدیمی نیز مشکلی ندارند، اما بسته به نسخه سوئیفت، ممکن است برخی از متدهایی که در این مقاله ارائه شدهاند نیاز به تغییر داشته باشند.
ایجاد یک پروژه جدید
قبل از هر چیز باید یک پروژه جدید Xcode ایجاد کنیم. به این منظور به آدرس File > New > Project... بروید یا کلیدهای ترکیبی CMD+Shift+N را بزنید.
گزینه macOS را به عنوان پلتفرم انتخاب کنید و سپس گزینه اول یعنی Cococa App را بزنید.
در این مرحله یک نام برای اپلیکیشن خود انتخاب کنید. ما در این راهنما «Advanced Clock» را انتخاب میکنیم و سپس مطمئن میشویم که گزینه Use Storyboards انتخاب شده باشد.
به طور معمول روی iOS از استوریبورد استفاده نمیکنیم، اما روی macOS این گزینه واقعاً مفید خواهد بود.
تنظیم پروژه
پیش از اقدام به کدنویسی باید برخی تنظیمات را روی پروژه خود اعمال کنیم. ابتدا باید مقدار پیشفرض WindowController و مقدار مرتبط ViewController را حذف کنیم، زیرا قصد نداریم در زمان شروع به کار برنامه پنجرهای باز شود. سپس باید چیزی مانند تصویر زیر و صرفاً یک Main Menu داشته باشیم.
نکته: Main Menu را حذف نکنید، چون این اقدام قابل بازگردانی نیست و اگر در ادامه بخواهید آن را خودتان بسازید، مسیری بسیار طولانی خواهد داشت.
کدنویسی پروژه
اینک باید اپلیکیشن ساعت خودمان را کدنویسی کنیم. تقریباً همه کد این اپلیکیشن در کلاس AppDelegate قرار میگیرد.
فایل AppDelegate.swift
1// Created by Federico Vitale on 20/05/2019.
2// Copyright © 2019 Federico Vitale. All rights reserved.
3//
4import Cocoa
5
6@NSApplicationMain
7class AppDelegate: NSObject, NSApplicationDelegate {
8
9
10
11 func applicationDidFinishLaunching(_ aNotification: Notification) {
12 // Insert code here to initialize your application
13 }
14
15 func applicationWillTerminate(_ aNotification: Notification) {
16 // Insert code here to tear down your application
17 }
18
19
20}
میتوان به راحتی متد applicationWillTerminate را حذف کرد، زیرا فعلاً نیازی به آن نداریم. سپس میتوانیم NSStatusItem را بسازیم که آیتم ما در «نوار وضعیت» (Status Bar) خواهد بود و به بیان دقیقتر کانتینر دکمه ما خواهد بود. برای ایجاد آیتم نوار وضعیت کد زیر را بنویسید.
فایل AppDelegate-01.swift
1//
2// AppDelegate.swift
3// Advanced Clock
4//
5// Created by Federico Vitale on 20/05/2019.
6// Copyright © 2019 Federico Vitale. All rights reserved.
7//
8import Cocoa
9
10@NSApplicationMain
11class AppDelegate: NSObject, NSApplicationDelegate {
12
13 var statusBarItem: NSStatusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
14
15 func applicationDidFinishLaunching(_ aNotification: Notification) {
16 guard let statusButton = statusBarItem.button else { return }
17 statusButton.title = "Advanced Clock"
18 }
19
20}
فعلاً یک متن placeholder به این کانتینر انتساب میدهیم و در ادامه نویت به زمانبندی یک تایمر میرسد که «عنوان» (title) را در هر ثانیه با مقدار زمان جاری بهروزرسانی میکند.
به این منظور متغیر کلاس دیگری به نام timer و با نوع optional به صورت Timer? اعلان میکنیم. مقدار آن را به صوت پیشفرض nil تعیین میکنیم. سپس در applicationDidFinisLaunching و زیر انتساب title تایمر را به صورت زیر کدنویسی میکنیم.
فایل AppDelegate-02.swift
1//
2// AppDelegate.swift
3// Advanced Clock
4//
5// Created by Federico Vitale on 20/05/2019.
6// Copyright © 2019 Federico Vitale. All rights reserved.
7//
8import Cocoa
9
10@NSApplicationMain
11class AppDelegate: NSObject, NSApplicationDelegate {
12 var statusBarItem: NSStatusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
13 var timer: Timer? = nil
14
15 func applicationDidFinishLaunching(_ aNotification: Notification) {
16 guard let statusButton = statusBarItem.button else { return }
17
18 statusButton.title = Date.now.stringTimeWithSeconds
19
20 timer = Timer.scheduledTimer(
21 timeInterval: 1,
22 target: self,
23 selector: #selector(updateStatusText),
24 userInfo: nil,
25 repeats: true
26 )
27 }
28
29 @objc
30 func updateStatusText(_ sender: Timer) {
31 guard let statusButton = statusBarItem.button else { return }
32 statusButton.title = Date.now.stringTimeWithSeconds
33 print(Date.now.stringTimeWithSeconds)
34 }
35}
همچنین یک تابع objective-c ایجاد میکنیم که هر بار رویداد تایمر اجرا شود (یعنی هر ثانیه یک بار) فراخوانی میشود. در ادامه برخی تابعهای کاربردی را میبینید که موجب میشوند همه چیز خواناتر و تمیزتر شود. یکی از این تابعها برای کلاس Date و دیگری برای Int است.
فایل Date+Extension.swift
1//
2// Date+Extension.swift
3// Advanced Clock
4//
5// Created by Federico Vitale on 20/05/2019.
6// Copyright © 2019 Federico Vitale. All rights reserved.
7//
8import Foundation
9
10
11/*
12 * -----------------------
13 * MARK: - Calendar Stuff
14 * ------------------------
15 */
16extension Date {
17 private var calendar: Calendar {
18 return Calendar.current
19 }
20
21 var weekDay: Int {
22 return calendar.component(.weekday, from: self)
23 }
24
25 var weekOfMonth: Int {
26 return calendar.component(.weekOfMonth, from: self)
27 }
28
29 var weekOfYear: Int {
30 return calendar.component(.weekOfYear, from: self)
31 }
32
33 var year: Int {
34 return calendar.component(.hour, from: self)
35 }
36
37 var month: Int {
38 return calendar.component(.month, from: self)
39 }
40
41 var quarter: Int {
42 return calendar.component(.quarter, from: self)
43 }
44
45 var day: Int {
46 return calendar.component(.day, from: self)
47 }
48
49 var era: Int {
50 return calendar.component(.era, from: self)
51 }
52
53 var hours: Int {
54 return calendar.component(.hour, from: self)
55 }
56
57 var minutes: Int {
58 return calendar.component(.minute, from: self)
59 }
60
61 var seconds: Int {
62 return calendar.component(.second, from: self)
63 }
64
65 var nanoseconds: Int {
66 return calendar.component(.nanosecond, from: self)
67 }
68}
69
70
71/*
72 * -----------------------
73 * MARK: - Utility
74 * ------------------------
75 */
76extension Date {
77 static var now: Date {
78 return self.init()
79 }
80
81 var stringTime: String {
82 return getStringTime(showSeconds: false)
83 }
84
85 var stringTimeWithSeconds: String {
86 return getStringTime(showSeconds: true)
87 }
88
89 var timestamp: TimeInterval {
90 return timeIntervalSince1970
91 }
92
93 private func getStringTime(showSeconds: Bool = false) -> String {
94 var time = "\(hours.safeString):\(minutes.safeString)"
95
96 if showSeconds {
97 time += ":\(seconds.safeString)"
98 }
99
100 return time
101 }
102}
فایل Int+Extension.swift
1//
2// Int+Extension.swift
3// Advanced Clock
4//
5// Created by Federico Vitale on 20/05/2019.
6// Copyright © 2019 Federico Vitale. All rights reserved.
7//
8import Foundation
9
10extension Int {
11 /// ---
12 /// var n: Int = 5
13 /// n = n.safeString
14 /// print(n) // "05"
15 /// ---
16 var safeString: String {
17 return self >= 10 ? "\(self)" : "0\(self)"
18 }
19}
اینک میتوانید اپلیکیشن خود را اجرا کنید و اگر همه کارها را به درستی انجام دهید، اکنون باید ساعت را در نوار وضعیت داشته باشید. بدین ترتیب شما موفق شدهاید نخستین اپلیکیشن macOS خود را بسازید. در ادامه آن را کمی جذابتر میسازیم.
افزودن منو
اکنون زمان آن رسیده است که اپلیکیشن خود را کاربرپسندتر بکنیم. بنابراین برخی قابلیتها و امکانات پیکربندی جدید به آن اضافه میکنیم.
ابتدا باید کدهایی بنویسیم که این ترجیحهای کاربر را مدیریت کنند. به این منظور معمولاً یک struct نوشته میشود که مقادیر پیشفرض کاربر (UserDefaults) را برای برخی تنظیمات ساده مانند اعداد بولی، رشتهها، enum-ها و موارد دیگر ذخیره میکند.
فایل Preferences.swift
1//
2// Preferences.swift
3// Advanced Clock
4//
5// Created by Federico Vitale on 20/05/2019.
6// Copyright © 2019 Federico Vitale. All rights reserved.
7//
8import Foundation
9
10fileprivate let defaults: UserDefaults = UserDefaults.standard
11
12struct Preferences {
13 static var useFlashDots: Bool {
14 get {
15 return defaults.bool(forKey: .useFlashDots)
16 }
17
18 set {
19 defaults.set(newValue, forKey: .useFlashDots)
20 defaults.synchronize()
21 }
22 }
23
24 static var showDockIcon: Bool {
25 get {
26 return defaults.bool(forKey: .showDockIcon)
27 }
28
29 set {
30 defaults.set(newValue, forKey: .showDockIcon)
31 defaults.synchronize()
32 }
33 }
34
35 static var showSeconds: Bool {
36 get {
37 return defaults.bool(forKey: .showSeconds)
38 }
39
40 set {
41 defaults.set(newValue, forKey: .showSeconds)
42 defaults.synchronize()
43 }
44 }
45
46 static var firstRunGone: Bool {
47 get {
48 return defaults.bool(forKey: .firstRunGone)
49 }
50
51 set {
52 defaults.set(newValue, forKey: .firstRunGone)
53 defaults.synchronize()
54 }
55 }
56
57 static func restore() {
58 Preferences.showSeconds = true
59 Preferences.useFlashDots = false
60 Preferences.showDockIcon = false
61 }
62}
فایل UserDefaults.swift
1//
2// Preferences.swift
3// Advanced Clock
4//
5// Created by Federico Vitale on 20/05/2019.
6// Copyright © 2019 Federico Vitale. All rights reserved.
7//
8import Foundation
9
10extension UserDefaults {
11 enum Key: String {
12 case useFlashDots = "useFlashDots"
13 case showSeconds = "showSeconds"
14 case showDockIcon = "showDockIcon"
15 case firstRunGone = "firstRunGone"
16 }
17
18 func set<T>(_ value: T, forKey key: Key) {
19 set(value, forKey: key.rawValue)
20 }
21
22 func bool(forKey key: Key) -> Bool {
23 return bool(forKey: key.rawValue)
24 }
25
26 func string(forKey key: Key) -> String? {
27 return string(forKey: key.rawValue)
28 }
29
30 func integer(forKey key: Key) -> Int? {
31 return integer(forKey: key.rawValue)
32 }
33
34 func url(forKey key: Key) -> URL? {
35 return url(forKey: key.rawValue)
36 }
37}
اکنون میتوانیم به طراحی منو بپردازیم. یک متغیر منوی جدید را در AppDelegate اعلان میکنیم و با استفاده از ()NSMenu یک منوی جدید میسازیم. سپس آیتمهایی مانند فهرست زیر به آن اضافه میکنیم:
- جداکنندههای چشمکزن
- نمایش/عدم نمایش ثانیه
- نمایش/عدم نمایش آیکون نوار وضعیت
- خروج از اپلیکیشن (کاملاً مفید است)
پیش از آن که به کدنویسی این امکانات بپردازیم، 2 اکستنشن دیگر را با شما به اشتراک میگذاریم که میتوانند به افزایش خوانایی و کاربردپذیری کد کمک کنند.
فایل Bool+Extension.swift
1//
2// Bool+Extension.swift
3// Advanced Clock
4//
5// Created by Federico Vitale on 20/05/2019.
6// Copyright © 2019 Federico Vitale. All rights reserved.
7//
8import Foundation
9import AppKit
10
11extension Bool {
12 var stateValue: NSControl.StateValue {
13 return self.toStateValue()
14 }
15
16 private func toStateValue() -> NSControl.StateValue {
17 return self ? .on : .off
18 }
19}
فایل NSMenu+Extension.swift
1//
2// NSMenu+Extension.swift
3// Advanced Clock
4//
5// Created by Federico Vitale on 20/05/2019.
6// Copyright © 2019 Federico Vitale. All rights reserved.
7//
8import Foundation
9import AppKit
10
11extension NSMenu {
12 func addSeparator() -> Void {
13 addItem(.separator())
14 }
15
16 func addItems(_ items: NSMenuItem...) {
17 for item in items {
18 addItem(item)
19 }
20 }
21}
بدین ترتیب اکنون AppDelegate را کمی بازسازی میکنیم تا کدها خواناتر و منسجمتر شوند. در نتیجه این کلاس به همراه پیادهسازی منو به صورت زیر درمیآید.
فایل AppDelegateWithMenu-01.swift
1//
2// AppDelegate.swift
3// Advanced Clock
4//
5// Created by Federico Vitale on 20/05/2019.
6// Copyright © 2019 Federico Vitale. All rights reserved.
7//
8import Cocoa
9
10@NSApplicationMain
11class AppDelegate: NSObject, NSApplicationDelegate {
12 var statusBarItem: NSStatusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
13 var timer: Timer? = nil
14
15 func applicationWillFinishLaunching(_ notification: Notification) {
16 if Preferences.firstRunGone == false {
17 // This will be executed on first run
18 Preferences.firstRunGone = true
19
20 // Set preferences to their defaults
21 Preferences.restore()
22 }
23 }
24
25 func applicationDidFinishLaunching(_ aNotification: Notification) {
26 guard let statusButton = statusBarItem.button else { return }
27
28 statusButton.title = Preferences.showSeconds ? Date.now.stringTimeWithSeconds : Date.now.stringTime
29
30 timer = Timer.scheduledTimer(
31 timeInterval: 1,
32 target: self,
33 selector: #selector(updateStatusText),
34 userInfo: nil,
35 repeats: true
36 )
37
38 let statusMenu: NSMenu = NSMenu()
39
40 statusMenu.addItem(withTitle: "Good \(Date.now.isMorning ? "Morning" : "Evening")", action: nil, keyEquivalent: "")
41 statusMenu.addSeparator()
42
43 let toggleFlashingSeparatorsItem: NSMenuItem = {
44 let item = NSMenuItem(
45 title: "Flashing separators",
46 action: #selector(toggleFlashingSeparators),
47 keyEquivalent: ""
48 )
49
50 item.tag = 1
51 item.target = self
52 item.state = Preferences.useFlashDots.stateValue
53
54 return item
55 }()
56
57 let toggleDockIconItem: NSMenuItem = {
58 let item = NSMenuItem(
59 title: "Toggle Dock Icon",
60 action: #selector(toggleDockIcon),
61 keyEquivalent: ""
62 )
63
64 item.tag = 2
65 item.target = self
66 item.state = Preferences.showDockIcon.stateValue
67
68 return item
69 }()
70
71 let toggleSecondsItem: NSMenuItem = {
72 let item = NSMenuItem(
73 title: "Show seconds",
74 action: #selector(toggleSeconds),
75 keyEquivalent: ""
76 )
77
78 item.tag = 3
79 item.target = self
80 item.state = Preferences.showSeconds.stateValue
81
82 return item
83 }()
84
85 let quitApplicationItem: NSMenuItem = {
86 let item = NSMenuItem(title: "Quit", action: #selector(terminate), keyEquivalent: "q")
87 item.target = self
88
89 return item
90 }()
91
92 statusMenu.addItems(
93 toggleFlashingSeparatorsItem,
94 toggleDockIconItem,
95
96 .separator(),
97
98 toggleSecondsItem,
99
100 .separator(),
101
102 quitApplicationItem
103 )
104
105 statusBarItem.menu = statusMenu
106 }
107}
108
109
110
111
112/*
113 * -----------------------
114 * MARK: - Actions
115 * ------------------------
116 */
117extension AppDelegate {
118 @objc
119 func updateStatusText(_ sender: Timer) {
120 guard let statusButton = statusBarItem.button else { return }
121 statusButton.title = Preferences.showSeconds ? Date.now.stringTimeWithSeconds : Date.now.stringTime
122 }
123
124 @objc
125 func toggleFlashingSeparators(_ sender: NSMenuItem) {
126 Preferences.useFlashDots = !Preferences.useFlashDots
127
128 if let menu = statusBarItem.menu, let item = menu.item(withTag: 1) {
129 item.state = Preferences.useFlashDots.stateValue
130 }
131 }
132
133
134 @objc
135 func toggleDockIcon(_ sender: NSMenuItem) {
136 Preferences.showDockIcon = !Preferences.showDockIcon
137
138 if let menu = statusBarItem.menu, let item = menu.item(withTag: 2) {
139 item.state = Preferences.showDockIcon.stateValue
140 }
141 }
142
143 @objc
144 func toggleSeconds(_ sender: NSMenuItem) {
145 Preferences.showSeconds = !Preferences.showSeconds
146
147 if let menu = statusBarItem.menu, let item = menu.item(withTag: 3) {
148 item.title = "Show seconds"
149 item.state = Preferences.showSeconds.stateValue
150 }
151 }
152
153 @objc
154 func terminate(_ sender: NSMenuItem) {
155 NSApp.terminate(sender)
156 }
157}
اپلیکیشن ما اینک به صورت زیر نمایش پیدا میکند:
اما این اپلیکیشن همچنان از نبود برخی قابلیتها رنج میبرد.
آیتمهای useFlashingDots و showDockIcon در حال حاضر بیاستفاده هستند. به جای این تابعها، تنظیمات showSeconds در تابع updateStatusText مدیریت میشود.
درون هر اکشن NSMenuItem باید خود آیتم را با ترجیحهای جدید بهروزرسانی کنیم، چون اینها کادرهای انتخابی هستند که موجب بهروزرسانی حالت میشوند. stateValue در Bool+Extension.swift گزارهای است که بسته به مقدار بولی true یا false یک مقدار on. یا off. بازگشت میدهد. بدین ترتیب دیگر نیازی به نوشتن Preferences.showDockIcon?.on: به جای Preferences.showDockIcon.stateValue نداریم.
ترجیحها: آیکون dock
اکنون یک اپلیکیشن نوار وضعیت داریم، اما یک آیکون dock نیز داریم. بدین ترتیب به عنان یک اپلیکیشن نوار وضعیت انتظار نداریم که آیکون dock نمایان باشد. اگر به گوشه راست-بالای نمایشگر خود نگاه کنید، این آیکون را میبینید.
تصور کنید اگر همه اپلیکیشنها در نوار وضعیت بخواهند آیکونی در dock داشته باشند چه قدر بههمریخته خواهد شد. ما میتوانیم آیکون dock را با NSApplication.ActivationPolicy مدیریت کنیم. به این منظور یک helper کوچک مینویسیم که یک struct با 2 متد برای تنظیم/خواندن ActivationPolicy است.
فایل DockIcon.swift
1//
2// DockIcon.swift
3// Advanced Clock
4//
5// Created by Federico Vitale on 20/05/2019.
6// Copyright © 2019 Federico Vitale. All rights reserved.
7//
8import Foundation
9import AppKit
10
11struct DockIcon {
12 static var standard = DockIcon()
13
14 var isVisible: Bool {
15 get {
16 return NSApp.activationPolicy() == .regular
17 }
18
19 set {
20 setVisibility(isVisible)
21 }
22 }
23
24 @discardableResult
25 func setVisibility(_ state: Bool) -> Bool {
26 if state {
27 NSApp.setActivationPolicy(.regular)
28 } else {
29 NSApp.setActivationPolicy(.accessory)
30 }
31
32 return isVisible
33 }
34}
اینک میتوانیم متد toggleDockIcon را ویرایش کنیم تا این ترجیح را مدیریت کند. در ادامه پیادهسازی این متد کمکی را میبینید:
فایل DockIconImplementation.swift
1// ... AppDelegate
2@objc
3func toggleDockIcon(_ sender: NSMenuItem) {
4 Preferences.showDockIcon = !Preferences.showDockIcon
5
6 DockIcon.standard.setVisibility(Preferences.showDockIcon)
7
8 if let menu = statusBarItem.menu, let item = menu.item(withTag: 2) {
9 item.state = Preferences.showDockIcon.stateValue
10 }
11}
چنان که میبینید در خط 7 helper مربوط به DockIcon را پیادهسازی کردهایم. اکنون این متد را کپی کرده و در انتهای متد applicationWillFinishLaunching میچسبانیم.
فایل applicationWillFinishLaunching.swift
1// ... AppDelegate
2func applicationWillFinishLaunching(_ notification: Notification) {
3 if Preferences.firstRunGone == false {
4 // This will be executed on first run
5 Preferences.firstRunGone = true
6
7 // Set preferences to their defaults
8 Preferences.restore()
9 }
10
11
12 DockIcon.standard.setVisibility(Preferences.showDockIcon)
13}
ترجیحها: جداکننده چشمکزن
در این بخش اقدام به پیادهسازی یکی از ترجیحهای کاربر یعنی جداکنندههای چشمکزن میکنیم. بدین ترتیب میتوانیم کارکردهای یک ساعت استاندارد را بسط بدهیم. در ادامه این قابلیت ساده را که حتی ساعت پیشفرض اپل هم آن را دارد پیادهسازی میکنیم.
این کار سادهتر از آن چیزی است که فکر میکنید. ما به یک متغیر نیاز داریم که وضعیت جداکنندهها را مدیریت کند. این متغیر میتواند به صورت بولی visible/hidden یا حتی 0/1 باشد. همچنین میتوانیم از NSControl.StateValue استفاده کنیم. اگر مقدار این متغیر 0 باشد یک جایگزینی رشته به صورت “:” -> “ “ در title صورت میگیرد:
فایل updateStatusText-01.swift
1//... AppDelegate
2var separatorsStatus: NSControl.StateValue = .on
3
4// or
5var separatorsStatus: Int = 1
6
7// or
8var separatorsStatus: Bool = true
9
10
11// Then check if the variable is truthy or not
12/// Implementation with `NSControl.StatusValue`
13@objc
14func updateStatusText(_ sender: Timer) {
15 guard let statusButton = statusBarItem.button else { return }
16 var title: String = (Preferences.showSeconds ? Date.now.stringTimeWithSeconds : Date.now.stringTime)
17
18 if Preferences.useFlashDots && separatorsStatus == .on {
19 separatorsStatus = .off
20 title = title.replacingOccurrences(of: ":", with: " ")
21 } else {
22 separatorsStatus = .on
23 }
24
25 statusButton.title = title
26}
یک روش بهتر برای انجام این کار بررسی این واقعیت است که ثانیهها زوج یا فرد هستند. هر زمان که عدد ثانیه زوج (یا فرد) باشد از این متغیر جدید به جای حالت پیشفرض استفاده میشود. همچنین این وضعیت در حالتی مناسب خواهد بود که بخواهید جداکننده هر ثانیه یک بار چشمک بزند.
فایل updateStatusText-02.swift
1// ... AppDelegate
2// Cleaner implementation
3@objc
4func updateStatusText(_ sender: Timer) {
5 guard let statusButton = statusBarItem.button else { return }
6 var title: String = (Preferences.showSeconds ? Date.now.stringTimeWithSeconds : Date.now.stringTime)
7
8 if Preferences.useFlashDots && Date.now.seconds % 2 == 0 {
9 title = title.replacingOccurrences(of: ":", with: " ")
10 }
11
12 statusButton.title = title
13}
در ادامه ظاهر ساعت را در حالت استفاده از جداکنندههای چشمکزن میبینید:
افزودن یادآوری
اینک اپلیکیشن ما به ظاهر تکمیل شده است، اما ما یکی از قابلیتهای اصلی اپلیکیشن خودمان را که تنظیم یادآوری بود فراموش کردهایم. در واقع چرا باید یک نفر بخواهد از اپلیکیشن ما صرفاً برای مشاهده ساعت استفاده کند؟ بنابراین باید قابلیت یادآوری را نیز به آن اضافه کنیم. این کار کاملاً ساده است و در طی آن با روش ایجاد و نمایش پنجرهها نیز آشنا میشویم.
کار خود را با ایجاد یک نوع struct و نامگذاری آن به صورت Reminder آغاز میکنیم:
فایل Reminder.swift
1//
2// Reminder.swift
3// Advanced Clock
4//
5// Created by Federico Vitale on 20/05/2019.
6// Copyright © 2019 Federico Vitale. All rights reserved.
7//
8import Foundation
9
10// We'll use this later
11protocol ReminderDelegate {
12 func onReminderFired(_ reminder: Reminder) -> Void
13}
14
15
16class Reminder {
17 private typealias Notification = NSUserNotification
18
19 var timer: Timer!
20 var title: String!
21 var descr: String?
22
23 var tag: Int?
24 var fireDate: Date
25 var delegate: ReminderDelegate?
26
27
28 init(_ title: String, description descr: String? = nil, fireOnDate date: Date, tag: Int? = nil) {
29 self.title = title
30 self.fireDate = date
31 self.tag = tag
32 self.descr = descr
33
34 self.timer = Timer.scheduledTimer(
35 withTimeInterval: date.timeIntervalSinceNow,
36 repeats: false,
37 block: { (t) in
38 t.invalidate()
39
40 let notification = Notification()
41
42 notification.title = self.title
43 notification.informativeText = self.descr
44 notification.subtitle = "Hey! Your timer has fired"
45
46 NSUserNotificationCenter.default.deliver(notification)
47
48 // DELEGATION
49 if let d = self.delegate {
50 d.onReminderFired(self)
51 }
52 }
53 )
54 }
55}
سپس یک متغیر جدید از نوع [Reminder] میسازیم و یک آرایه خالی با مقدار پیشفرض اضافه میکنیم.
1var reminders: [Reminder] = []
اکنون میتوانیم منطق آن را نیز بنویسیم:
- یک آیتم منو به منوی استارت اضافه کن
- یک منوی فرعی برای همه یادآوریها اضافه کن.
- یکم آیتم منو اضافه کن که کلیک کردن آن موجب باز شدن پنجرهای برای زمانبندی یک یادآور جدید میشود.
- مدیریت یادآوری و کنترل view را به AppDelegate اعطا کن.
ایجاد NSMenuItem و submenu
همان طور که تا به این جا در مورد عناصر دیگر انجام دادهایم ابتدا یک NSMenuItem میسازیم و این مورد شبیه به statusBarItem خواهد بود. بدین ترتیب یک مشخصه منو خواهد داشت که در آن همه یادآوریهای درون متغیر reminders که قبلاً اعلان کردهایم، نمایش پیدا میکنند.
فایل reminder-submenu.swift
1let remindersItem: NSMenuItem = {
2 let item = NSMenuItem(title: "Reminders", action: nil, keyEquivalent: "")
3 item.tag = 5
4
5 let menu = NSMenu()
6
7 for reminder in self.reminders {
8 menu.addItem(.init(title: reminder.title, action: nil, keyEquivalent: ""))
9 }
10
11 item.isEnabled = reminders.count > 0
12
13 return item
14}()
شیوه افزودن یادآور جدید
اکنون میخواهیم یک اینترفیس برای افزودن یادآوری بنویسیم. به این منظور Main.storyboard را باز کرده و یک view controller بسازید که عنوان آن NSTextField، توضیح آن NSTextView و تاریخ آن NSDatePicker باشد.
این کار از طریق کدنویسی و به صورت برنامهنویسی شده نیز ممکن است، اما آن مسیر طولانی است و به دلیل مقاصد آموزشی در این راهنما از سریعترین روش برای طراحی یک پنجره ابتدایی با برخی کنترلها استفاده میکنیم.
اکنون نوبت به ایجاد فایل رسیده است. این فایل کنترلر نمای ما خواهد بود. اگر فایل ViewController را حذف نکرده باشید، باید همچنان در پروژه موجود باشد و میتوانید به جای ایجاد فایل جدید آن را ویرایش کنید. ابتدا نام آن را به NewReminderVC تغییر دهید و نام کلاس درون آن را نیز به همین ترتیب عوض کنید. بدین ترتیب چیزی مانند زیر خواهید داشت:
فایل NewReminderVC-01.swift
1//
2// NewRemainderVC.swift
3// Advanced Clock
4//
5// Created by Federico Vitale on 20/05/2019.
6// Copyright © 2019 Federico Vitale. All rights reserved.
7//
8import Cocoa
9
10protocol NewReminderVCDelegate {
11 func onSubmit(_ sender: NSButton, reminder: Reminder) -> Void
12}
13
14class NewReminderVC: NSViewController {
15 override func viewDidLoad() {
16 super.viewDidLoad()
17 }
18}
برای صرفهجویی در زمان یک Delegate Protocol به ابتدای فایل اضافه میکنیم که از آن برای مدیریت رویداد submit استفاده خواهیم کرد.
اکنون همه موارد تنظیم شدهاند. بنابراین به استوریبورد بروید و کنترلر نما را که هم اینک ایجاد کردهایم انتخاب کرده و کلاس جدید را به آن انتساب دهید:
مطمئن شوید که یک Storyboard ID اضافه کردهاید. ما آن را کلاس view controller نامیدهایم. از این شناسه استوریبورد در ادامه برای شناسایی نمای خود در کد استفاده میکنیم.
اکنون با انتخاب کردن view controller روی دکمه Assistant Editor در گوشه راست-بالای Xcخde کلیک میکنیم.
بدین ترتیب ویرایشگر به دو بخش تقسیم میشود:
اطمینان حاصل کنید که فایل درستی را انتخاب کردهاید (در واقع فایل درست، فایل کنترلر نما است)، سپس آیتمهای خود را (با فشردن کلید ctrl) در کلاس کشیده و رها کنید.
ما اینک باید 3 IBOutlets و 1 IBAction داشته باشیم. اکشن به دکمه متصل شده است و 3 outlets دیگر به ترتیب title ،description و date picker هستند.
بدین ترتیب در حال حاضر یک نما یا view داریم، اما چگونه میتوانیم آن را نمایش دهیم؟ این کار آسان است، ابتدا یک فایل جدید به کمک این helper بسازید. بدین ترتیب تابعی با نام getVC به دست میآید که باید شناسه استوریبورد و کلاس کنترل نما را به آن ارسال کنید.
فایل WindowsManager.swift
1//
2// WindowsManager.swift
3// Advanced Clock
4//
5// Created by Federico Vitale on 20/05/2019.
6// Copyright © 2019 Federico Vitale. All rights reserved.
7//
8import Foundation
9import AppKit
10
11struct WindowsManager {
12 static func getVC<T: NSViewController>(withIdentifier identifier: String, ofType: T.Type?, storyboard: String = "Main", bundle: Bundle? = nil) -> T? {
13 let storyboard = NSStoryboard(name: storyboard, bundle: bundle)
14
15 guard let vc: T = storyboard.instantiateController(withIdentifier: identifier) as? T else {
16 let alert = NSAlert()
17 alert.alertStyle = .critical
18 alert.messageText = "Error initiating the viewcontroller"
19 alert.runModal()
20
21 return nil
22 }
23
24 return vc
25 }
26}
اکنون تابع مفقود در AppDelegate را اضافه میکنیم. نخستین کاری که باید انجام دهیم اعلان کردن یک ثابت جدید در ابتدای فایل است:
1let REMINDERS_WINDOW_CONTROLLER: NSWindowController = NSWindowController(window: nil)
اکنون یک اکشن را کم داریم و بنابراین آن را اضافه میکنیم. اما قبل از آن باید متغیر reminders را ویرایش کرده و یک رویداد didSet به آن اضافه کنیم:
فایل AppDelegate@AddReminder.swift
1var reminders: [Reminder] = [] {
2 didSet {
3 if let menu = statusBarItem.menu, let item = menu.item(withTag: 5) {
4 item.submenu = self.getRemindersMenu()
5 item.isEnabled = reminders.count > 0
6 }
7 }
8}
9
10
11/*
12 * -----------------------
13 * MARK: - Utilities
14 * ------------------------
15 */
16extension AppDelegate {
17 private func getRemindersMenu() -> NSMenu {
18 let menu = NSMenu()
19
20 for reminder in self.reminders {
21 menu.addItem(.init(title: reminder.title, action: nil, keyEquivalent: ""))
22 }
23
24 return menu
25 }
26}
بدین ترتیب یک تابع کاربردی ساختهایم که به سادگی منوی یادآوریها را برای ما ایجاد میکند. اکنون باید آیتمهای منو را بسازیم و همچنین اضافه کردن آنها به منو را نیز نباید فراموش کنیم:
فایل addRemainderItem.swift
1let addReminderItem: NSMenuItem = {
2 let item = NSMenuItem(title: "New Reminder", action: #selector(addReminder), keyEquivalent: "")
3 item.tag = 6
4 item.target = self
5 return item
6}()
در حال حاضر addReminder همچنان اعلان نشده است و از این رو Xcode پیام خطایی را نمایش میدهد. ابتدا آن را اعلان میکنیم تا خطا رفع شود.
فایل addReminderFunction.swift
1@objc
2func addReminder(_ sender: NSMenuItem) {
3 if let vc = WindowsManager.getVC(withIdentifier: "NewReminderVC", ofType: NewReminderVC.self) {
4 vc.delegate = self
5 let window: NSWindow = {
6 let w = NSWindow(contentViewController: vc)
7
8 w.styleMask.remove(.fullScreen)
9 w.styleMask.remove(.resizable)
10 w.styleMask.remove(.miniaturizable)
11
12 w.level = .floating
13
14 return w
15 }()
16
17 if REMINDERS_WINDOW_CONTROLLER.window == nil {
18 REMINDERS_WINDOW_CONTROLLER.window = window
19 }
20
21 REMINDERS_WINDOW_CONTROLLER.showWindow(self)
22 }
23}
برای درک سطح floating به توضیح زیر توجه کنید:
«سطوح پنجره» (Window Levels) دارای یک لیست از مقادیر ممکن است. هر سطح در لیست، پنجرههای خود را در جلوی گروههای قبلی گروهبندی میکند. برای نمونه پنجرههای شناور (Floating) در جلوی همه پنجرههای سطح نرمال ظاهر میشوند. هنگامی که یک پنجره وارد سطح جدیدی میشود، در جلوی همه همتایانش در آن سطح مرتبسازی خواهد شد.
نکته مهم دیگر این که باید delegate-ها را پیادهسازی کنیم و کارهایی را نیز درون NewReminderVC سامان دهیم.
فایل NewReminderVC-02.swift
1//
2// NewReminderVC.swift
3// Advanced Clock
4//
5// Created by Federico Vitale on 20/05/2019.
6// Copyright © 2019 Federico Vitale. All rights reserved.
7//
8import Cocoa
9
10
11protocol NewReminderVCDelegate {
12 func onSubmit(_ sender: NSButton, reminder: Reminder) -> Void
13}
14
15class NewReminderVC: NSViewController {
16 var delegate: NewReminderVCDelegate!
17
18 @IBOutlet weak var taskTitle: NSTextField!
19 @IBOutlet weak var taskDate: NSDatePicker!
20 @IBOutlet weak var taskDescr: NSTextView!
21
22 @IBAction func onSubmit(_ sender: NSButton) {
23 let reminder = Reminder(
24 taskTitle.stringValue,
25 description: taskDescr.string,
26 fireOnDate: taskDate.dateValue,
27 tag: nil
28 )
29
30 delegate.onSubmit(sender, reminder: reminder)
31 }
32
33
34 override func viewDidLoad() {
35 super.viewDidLoad()
36
37 taskTitle.stringValue = ""
38 taskDescr.string = ""
39
40 taskDate.calendar = Calendar.current
41 taskDate.dateValue = Date.now
42 }
43}
در متد viewDidLoad مقادیر پیشفرض را تعیین میکنیم و سپس reminder خود را ایجاد میکنیم. در ادامه متد onSubmit مربوط به delegate را فراخوانی میکنیم.
Delegation
در نهایت باید delegation را از NewReminderVC تأیید کنیم، به این منظور یک افزونه از AppDelegate مانند زیر اضافه میکنیم:
فایل AppDelegateDelegation-02.swift
1extension AppDelegate: NewReminderVCDelegate, ReminderDelegate {
2
3 // On submit close the window and save the reminder
4 func onSubmit(_ sender: NSButton, reminder: Reminder) {
5 reminder.delegate = self
6 if reminder.tag == nil {
7 reminder.tag = reminders.count
8 }
9
10 REMINDERS_WINDOW_CONTROLLER.close()
11 reminders.append(reminder)
12 }
13
14 // Once a reminder is fired, let's delete it
15 func onReminderFired(_ reminder: Reminder) {
16 reminders.removeAll(where: { $0.tag == reminder.tag })
17 }
18}
سخن پایانی
بدین ترتیب به پایان این مقاله راهنمای ساخت نخستین اپلیکیشن macOS میرسیم. شما میتوانید کد کامل این پروژه را در این آدرس گیتهاب (+) ملاحظه کنید.
اکنون شما میتوانید نخستین اپلیکیشن macOS خود را که نوشتهاید اجرا کنید. این مقاله یک راهنمای مقدماتی برای توسعه اپلیکیشنهای مک محسوب میشود و با تمرین بیشتر و استفاده از منابع دیگر میتوانید مهارتهای خود را در این زمینه بیش از پیش افزایش دهید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- مجموعه آموزشهای پروژه محور برنامهنویسی
- مجموعه آموزشهای دروس علوم و مهندسی کامپیوتر
- دستورهای ترمینال سیستم عامل مک (macOS) — راهنمای مقدماتی
- آموزش جامع و مقدماتی مک او اس – بخش اول: آشنایی با رابط کاربری و امکانات اولیه
==