توسعه یک اپلیکیشن MacOS ساده — از صفر تا صد

۹۶ بازدید
آخرین به‌روزرسانی: ۲۹ شهریور ۱۴۰۲
زمان مطالعه: ۱۵ دقیقه
توسعه یک اپلیکیشن MacOS ساده — از صفر تا صد

در این مقاله قصد داریم با روش توسعه یک اپلیکیشن MacOS ساده آشنا شویم. این اپلیکیشن یک ساعت در نوار وضعیت سیستم عامل خواهد بود که امکان تنظیم یادآور دارد و کاربر می‌تواند نوتیفیکیشن‌ها را در آن زمان‌بندی کند. در این فرایند با روش جلوگیری از باز شدن یک پنجره برای چندین بار، تنظیم نوتیفیکیشن و موارد دیگر آشنا خواهیم شد.

پیش‌نیازها برای توسعه اپلیکیشن MacOS

برای مطالعه این راهنما، برخی موارد نیاز هستند که در ادامه فهرست شده‌اند:

  • دانش مقدماتی از زبان برنامه‌نویسی سوئیفت.
  • یک سیستم مک که روی آن Xcode نصب شده است.
  • نسخه Xcode باید 10.2 یا بالاتر باشد. گرچه نسخه‌های قدیمی نیز مشکلی ندارند، اما بسته به نسخه سوئیفت، ممکن است برخی از متدهایی که در این مقاله ارائه شده‌اند نیاز به تغییر داشته باشند.

ایجاد یک پروژه جدید

قبل از هر چیز باید یک پروژه جدید Xcode ایجاد کنیم. به این منظور به آدرس File > New > Project... بروید یا کلیدهای ترکیبی CMD+Shift+N را بزنید.

اپلیکیشن MacOS

گزینه macOS را به عنوان پلتفرم انتخاب کنید و سپس گزینه اول یعنی Cococa App را بزنید.

اپلیکیشن MacOS

در این مرحله یک نام برای اپلیکیشن خود انتخاب کنید. ما در این راهنما «Advanced Clock» را انتخاب می‌کنیم و سپس مطمئن می‌شویم که گزینه Use Storyboards انتخاب شده باشد.

به طور معمول روی iOS از استوری‌بورد استفاده نمی‌کنیم، اما روی macOS این گزینه واقعاً مفید خواهد بود.

تنظیم پروژه

پیش از اقدام به کدنویسی باید برخی تنظیمات را روی پروژه خود اعمال کنیم. ابتدا باید مقدار پیش‌فرض WindowController و مقدار مرتبط ViewController را حذف کنیم، زیرا قصد نداریم در زمان شروع به کار برنامه پنجره‌ای باز شود. سپس باید چیزی مانند تصویر زیر و صرفاً یک Main Menu داشته باشیم.

نکته: Main Menu را حذف نکنید، چون این اقدام قابل بازگردانی نیست و اگر در ادامه بخواهید آن را خودتان بسازید، مسیری بسیار طولانی خواهد داشت.

اپلیکیشن MacOS

کدنویسی پروژه

اینک باید اپلیکیشن ساعت خودمان را کدنویسی کنیم. تقریباً همه کد این اپلیکیشن در کلاس 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}

اپلیکیشن ما اینک به صورت زیر نمایش پیدا می‌کند:

اپلیکیشن MacOS

اما این اپلیکیشن همچنان از نبود برخی قابلیت‌ها رنج می‌برد.

آیتم‌های useFlashingDots و showDockIcon در حال حاضر بی‌استفاده هستند. به جای این تابع‌ها، تنظیمات showSeconds در تابع updateStatusText مدیریت می‌شود.

درون هر اکشن NSMenuItem باید خود آیتم را با ترجیح‌های جدید به‌روزرسانی کنیم، چون این‌ها کادرهای انتخابی هستند که موجب به‌روزرسانی حالت می‌شوند. stateValue در Bool+Extension.swift گزاره‌ای است که بسته به مقدار بولی true یا false یک مقدار on. یا off. بازگشت می‌دهد. بدین ترتیب دیگر نیازی به نوشتن Preferences.showDockIcon?.on: به جای Preferences.showDockIcon.stateValue نداریم.

ترجیح‌ها: آیکون dock

اکنون یک اپلیکیشن نوار وضعیت داریم، اما یک آیکون dock نیز داریم. بدین ترتیب به عنان یک اپلیکیشن نوار وضعیت انتظار نداریم که آیکون dock نمایان باشد. اگر به گوشه راست-بالای نمایشگر خود نگاه کنید، این آیکون را می‌بینید.

اپلیکیشن MacOS

تصور کنید اگر همه اپلیکیشن‌ها در نوار وضعیت بخواهند آیکونی در 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}

اپلیکیشن MacOS

ترجیح‌ها: جداکننده چشمک‌زن

در این بخش اقدام به پیاده‌سازی یکی از ترجیح‌های کاربر یعنی جداکننده‌های چشمک‌زن می‌کنیم. بدین ترتیب می‌توانیم کارکردهای یک ساعت استاندارد را بسط بدهیم. در ادامه این قابلیت ساده را که حتی ساعت پیش‌فرض اپل هم آن را دارد پیاده‌سازی می‌کنیم.

این کار ساده‌تر از آن چیزی است که فکر می‌کنید. ما به یک متغیر نیاز داریم که وضعیت جداکننده‌ها را مدیریت کند. این متغیر می‌تواند به صورت بولی 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}

در ادامه ظاهر ساعت را در حالت استفاده از جداکننده‌های چشمک‌زن می‌بینید:

اپلیکیشن MacOS

افزودن یادآوری

اینک اپلیکیشن ما به ظاهر تکمیل شده است، اما ما یکی از قابلیت‌های اصلی اپلیکیشن خودمان را که تنظیم یادآوری بود فراموش کرده‌ایم. در واقع چرا باید یک نفر بخواهد از اپلیکیشن ما صرفاً برای مشاهده ساعت استفاده کند؟ بنابراین باید قابلیت یادآوری را نیز به آن اضافه کنیم. این کار کاملاً ساده است و در طی آن با روش ایجاد و نمایش پنجره‌ها نیز آشنا می‌شویم.

کار خود را با ایجاد یک نوع 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 باشد.

این کار از طریق کدنویسی و به صورت برنامه‌نویسی شده نیز ممکن است، اما آن مسیر طولانی است و به دلیل مقاصد آموزشی در این راهنما از سریع‌ترین روش برای طراحی یک پنجره ابتدایی با برخی کنترل‌ها استفاده می‌کنیم.

اپلیکیشن MacOS

اکنون نوبت به ایجاد فایل رسیده است. این فایل کنترلر نمای ما خواهد بود. اگر فایل 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 استفاده خواهیم کرد.

اکنون همه موارد تنظیم شده‌اند. بنابراین به استوری‌بورد بروید و کنترلر نما را که هم اینک ایجاد کرده‌ایم انتخاب کرده و کلاس جدید را به آن انتساب دهید:

اپلیکیشن MacOS

مطمئن شوید که یک Storyboard ID اضافه کرده‌اید. ما آن را کلاس view controller نامیده‌ایم. از این شناسه استوری‌بورد در ادامه برای شناسایی نمای خود در کد استفاده می‌کنیم.

اپلیکیشن MacOS

اکنون با انتخاب کردن view controller روی دکمه Assistant Editor در گوشه راست-بالای Xcخde کلیک می‌کنیم.

بدین ترتیب ویرایشگر به دو بخش تقسیم می‌شود:

اپلیکیشن MacOS

اطمینان حاصل کنید که فایل درستی را انتخاب کرده‌اید (در واقع فایل درست، فایل کنترلر نما است)، سپس آیتم‌های خود را (با فشردن کلید 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 خود را که نوشته‌اید اجرا کنید. این مقاله یک راهنمای مقدماتی برای توسعه اپلیکیشن‌های مک محسوب می‌شود و با تمرین بیشتر و استفاده از منابع دیگر می‌توانید مهارت‌های خود را در این زمینه بیش از پیش افزایش دهید.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۱ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
swlh
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *