您的位置:首页 > 移动开发 > Swift

使用Swift开发一个MacOS的菜单状态栏App

2016-11-20 21:34 471 查看
猴子原创,欢迎转载。转载请注明: 转载自Cocos2Der-CSDN,谢谢!  

原文地址: http://blog.csdn.net/cocos2der/article/details/52054107

这两天突然想看看OSX下的App开发,看了几篇文章。下面这一篇我觉得入门是非常好的。我仅转述为中文,并非原文翻译。原文地址:http://footle.org/WeatherBar/

下面开始介绍如何使用Swift开发一个Mac Menu Bar(Status Bar) App。通过做一个简单的天气app。天气数据来源于OpenWeatherMap

完成后的效果如下: 




一、开始建立工程

打开Xcode,Create a New Project or File ⟶ New ⟶ Project ⟶ Application ⟶ Cocoa Application ( OS X 这一栏)。点击下一步。 




二、开始代码工作

打开MainMenu.xib,删除默认的windows和menu菜单。因为我们是状态栏app,不需要菜单栏,不需要主窗口。 



添加一个Menu菜单 



删除其中默认的2个子菜单选项,仅保留1个。并将保留的这个改名为“Quit”。

打开双视图绑定Outlet

将Menu Outlet到AppDelegate,命名为statusMenu 



将子菜单Quit绑定Action到AppDelegate,命名为quitClicked 



你可以删除 
@IBOutlet weak var window:
NSWindow!
 ,这个app中用不上。

代码

在AppDelegate.swift中statusMenu下方添加
let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(NSVariableStatusItemLength)
1

applicationDidFinishLaunching函数中添加:
statusItem.title = "WeatherBar"
statusItem.menu = statusMenu
1
2

在quitClicked中添加:
NSApplication.sharedApplication().terminate(self)
1

此时你的代码应该如下:

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

@IBOutlet weak var statusMenu: NSMenu!

let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(NSVariableStatusItemLength)

@IBAction func quitClicked(sender: NSMenuItem) {
NSApplication.sharedApplication().terminate(self)
}

func applicationDidFinishLaunching(aNotification: NSNotification) {
statusItem.title = "WeatherBar" statusItem.menu = statusMenu
}

func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

运行,你可以看到一个状态栏了。


三、进阶一步,让App变得更好

你应该注意到了,当你运行后,底部Dock栏里出现了一个App启动的Icon。但实际上我们也不需要这个启动icon,打开Info,添加 “Application is agent (UIElement)”为YES。 



运行一下,不会出现dock启动icon了。


四、添加状态栏Icon

状态栏icon尺寸请使用18x18

, 36x36(@2x)

,
54x54(@3x),添加这1x和2x两张图到Assets.xcassets中。 



在applicationDidFinishLaunching中,修改为如下:
let icon = NSImage(named: "statusIcon")
icon?.template = true // best for dark mode
statusItem.image = icon
statusItem.menu = statusMenu
1
2
3
4

运行一下,你应该看到状态栏icon了。


五、重构下代码

如果我们进一步写下去,你会发现大量代码在AppDelegate中,我们不希望这样。下面我们为Menu创建一个Controller来管理。
新建一个NSObject的StatusMenuController.swift, File ⟶ New File ⟶ OS X Source ⟶ Cocoa Class ⟶ Next 



代码如下:
// StatusMenuController.swift

import Cocoa

class StatusMenuController: NSObject {
@IBOutlet weak var statusMenu: NSMenu!

let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(NSVariableStatusItemLength)

override func awakeFromNib() {
let icon = NSImage(named: "statusIcon") icon?.template = true // best for dark mode statusItem.image = icon statusItem.menu = statusMenu
}

@IBAction func quitClicked(sender: NSMenuItem) {
NSApplication.sharedApplication().terminate(self)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
还原AppDelegate,修改为如下:
// AppDelegate.swift

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(aNotification: NSNotification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13

注意,因为删除了AppDelegate中的Outlet注册,所以你需要重新连Outlet,但在这之前我们需要先做一件事。(你可以试试连接StatusMenuController中的Outlet,看看会怎么样?)
打开MainMenu.xib,添加一个Object。 



将该Object的Class指定为StatusMenuController 



重建Outlet到StatusMenuController,注意删除之前连接到AppDelegate的Outlet 



当MainMenu.xib被初始化的时候,StatusMenuController下的awakeFromNib将会被执行,所以我们在里面做初始化工作。

运行一下,保证你全部正常工作了。


六、天气Api

我们使用 OpenWeatherMap的天气数据,所以你得注册一个账号,获取到免费的API
Key。
添加WeatherAPI.swift, File ⟶ New File ⟶ OS X Source ⟶ Swift File ⟶ WeatherAPI.swift,加入如下代码,并使用你自己的API Key。
import Foundation

class WeatherAPI {
let API_KEY = "your-api-key-here"
let BASE_URL = "http://api.openweathermap.org/data/2.5/weather"

func fetchWeather(query: String) {
let session = NSURLSession.sharedSession()
// url-escape the query string we're passed
let escapedQuery = query.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())
let url = NSURL(string: "\(BASE_URL)?APPID=\(API_KEY)&units=imperial&q=\(escapedQuery!)")
let task = session.dataTaskWithURL(url!) { data, response, err in
// first check for a hard error
if let error = err {
NSLog("weather api error: \(error)")
}

// then check the response code
if let httpResponse = response as? NSHTTPURLResponse {
switch httpResponse.statusCode {
case 200: // all good!
let dataString = NSString(data: data!, encoding: NSUTF8StringEncoding) as! String
NSLog(dataString)
case 401: // unauthorized
NSLog("weather api returned an 'unauthorized' response. Did you set your API key?")
default:
NSLog("weather api returned response: %d %@", httpResponse.statusCode, NSHTTPURLResponse.localizedStringForStatusCode(httpResponse.statusCode))
}
}
}
task.resume()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

添加一个Update子菜单到Status Menu。 



绑定Action到StatusMenuController.swift,取名为updateClicked

开始使用WeatherAPI, 在StatusMenuController中let statusItem下面加入: 
let weatherAPI = WeatherAPI()
,  

在updateClicked中加入: 
weatherAPI.fetchWeather("Seattle")


注意OSX 10.11之后请添加NSAppTransportSecurity,保证http能使用。

运行一下,然后点击Update菜单。你会收到一个json格式的天气数据。
我们再调整下StatusMenuController代码, 添加一个updateWeather函数,修改后如下:
import Cocoa

class StatusMenuController: NSObject {
@IBOutlet weak var statusMenu: NSMenu!

let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(NSVariableStatusItemLength)
let weatherAPI = WeatherAPI()

override func awakeFromNib() {
statusItem.menu = statusMenu
let icon = NSImage(named: "statusIcon") icon?.template = true // best for dark mode statusItem.image = icon statusItem.menu = statusMenu

updateWeather()
}

func updateWeather() {
weatherAPI.fetchWeather("Seattle")
}

@IBAction func updateClicked(sender: NSMenuItem) {
updateWeather()
}

@IBAction func quitClicked(sender: NSMenuItem) {
NSApplication.sharedApplication().terminate(self)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30


七、解析Json

你可以使用 SwiftyJSON,但本次我们先不使用第三方库。我们得到的天气数据如下:
{
"coord": {
"lon": -122.33,
"lat": 47.61
},
"weather": [{
"id": 800,
"main": "Clear",
"description": "sky is clear",
"icon": "01n"
}],
"base": "cmc stations",
"main": {
"temp": 57.45,
"pressure": 1018,
"humidity": 59,
"temp_min": 53.6,
"temp_max": 62.6
},
"wind": {
"speed": 2.61,
"deg": 19.5018
},
"clouds": {
"all": 1
},
"dt": 1444623405,
"sys": {
"type": 1,
"id": 2949,
"message": 0.0065,
"country": "US",
"sunrise": 1444659833,
"sunset": 1444699609
},
"id": 5809844,
"name": "Seattle",
"cod": 200
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
在WeatherAPI.swift添加天气结构体用于解析son
struct Weather {
var city: String
var currentTemp: Float
var conditions: String
}
1
2
3
4
5
解析son
func weatherFromJSONData(data: NSData) -> Weather? {
typealias JSONDict = [String:AnyObject]
let json : JSONDict

do {
json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as! JSONDict
} catch {
NSLog("JSON parsing failed: \(error)")
return nil
}

var mainDict = json["main"] as! JSONDict
var weatherList = json["weather"] as! [JSONDict]
var weatherDict = weatherList[0]

let weather = Weather(
city: json["name"] as! String,
currentTemp: mainDict["temp"] as! Float,
conditions: weatherDict["main"] as! String
)

return weather
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
修改fetchWeather函数去调用weatherFromJSONData
let task = session.dataTaskWithURL(url!) { data, response, error in
// first check for a hard error
if let error = err {
NSLog("weather api error: \(error)")
}

// then check the response code
if let httpResponse = response as? NSHTTPURLResponse {
switch httpResponse.statusCode {
case 200: // all good!
if let weather = self.weatherFromJSONData(data!) {
NSLog("\(weather)")
}
case 401: // unauthorized
NSLog("weather api returned an 'unauthorized' response. Did you set your API key?")
default:
NSLog("weather api returned response: %d %@", httpResponse.statusCode, NSHTTPURLResponse.localizedStringForStatusCode(httpResponse.statusCode))
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

如果此时你运行,你会收到
2016-07-28 11:25:08.457 WeatherBar[49688:1998824] Optional(WeatherBar.Weather(city: "Seattle", currentTemp: 51.6, conditions: "Clouds"))
1
给Weather结构体添加一个description
struct Weather: CustomStringConvertible {
var city: String
var currentTemp: Float
var conditions: String

var description: String {
return "\(city): \(currentTemp)F and \(conditions)"
}
}
1
2
3
4
5
6
7
8
9

再运行试试。


八、Weather用到Controller中

在 WeatherAPI.swift中增加delegate协议
protocol WeatherAPIDelegate {
func weatherDidUpdate(weather: Weather)
}
1
2
3

声明
var delegate: WeatherAPIDelegate?


添加初始化

init(delegate: WeatherAPIDelegate) {
self.delegate = delegate
}
1
2
3
修改fetchWeather
let task = session.dataTaskWithURL(url!) { data, response, error in
// first check for a hard error
if let error = err {
NSLog("weather api error: \(error)")
}

// then check the response code
if let httpResponse = response as? NSHTTPURLResponse {
switch httpResponse.statusCode {
case 200: // all good!
if let weather = self.weatherFromJSONData(data!) {
self.delegate?.weatherDidUpdate(weather)
}
case 401: // unauthorized
NSLog("weather api returned an 'unauthorized' response. Did you set your API key?")
default:
NSLog("weather api returned response: %d %@", httpResponse.statusCode, NSHTTPURLResponse.localizedStringForStatusCode(httpResponse.statusCode))
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
StatusMenuController添加WeatherAPIDelegate
class StatusMenuController: NSObject, WeatherAPIDelegate {
...
var weatherAPI: WeatherAPI!

override func awakeFromNib() {
...
weatherAPI = WeatherAPI(delegate: self)
updateWeather()
}
...
func weatherDidUpdate(weather: Weather) {
NSLog(weather.description)
}
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Callback实现,修改WeatherAPI.swift中fetchWeather: 
func fetchWeather(query: String,
success: (Weather) -> Void) {
 

修改fetchWeather内容
let task = session.dataTaskWithURL(url!) { data, response, error in
// first check for a hard error
if let error = err {
NSLog("weather api error: \(error)")
}

// then check the response code
if let httpResponse = response as? NSHTTPURLResponse {
switch httpResponse.statusCode {
case 200: // all good!
if let weather = self.weatherFromJSONData(data!) {
success(weather)
}
case 401: // unauthorized
NSLog("weather api returned an 'unauthorized' response. Did you set your API key?")
default:
NSLog("weather api returned response: %d %@", httpResponse.statusCode, NSHTTPURLResponse.localizedStringForStatusCode(httpResponse.statusCode))
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
在controller中
func updateWeather() {
weatherAPI.fetchWeather("Seattle, WA") { weather in
NSLog(weather.description)
}
}
1
2
3
4
5

运行一下,确保都正常。


九、显示天气

在MainMenu.xib中添加子菜单 “Weather”(你可以添加2个Separator Menu Item用于子菜单分割线) 



在updateWeather中,替换NSLog:
if let weatherMenuItem = self.statusMenu.itemWithTitle("Weather") {
weatherMenuItem.title = weather.description
}
1
2
3

运行一下,看看天气是不是显示出来了。


十、创建一个天气视图

打开MainMenu.xib,拖一个Custom View进来。

拖一个Image View到Custom View中,设置ImageView宽高度为50。 



拖两个Label进来,分别为City和Temperature 



创建一个名为WeatherView的NSView,New File ⟶ OS X Source ⟶ Cocoa Class 

在MainMenu.xib中,将Custom View的Class指定为WeatherView 



绑定WeatherView Outlet:

import Cocoa

class WeatherView: NSView {
@IBOutlet weak var imageView: NSImageView!
@IBOutlet weak var cityTextField: NSTextField!
@IBOutlet weak var currentConditionsTextField: NSTextField!
}
1
2
3
4
5
6
7

并添加update:
func update(weather: Weather) {
// do UI updates on the main thread
dispatch_async(dispatch_get_main_queue()) {
self.cityTextField.stringValue = weather.city
self.currentConditionsTextField.stringValue = "\(Int(weather.currentTemp))°F and \(weather.conditions)"
self.imageView.image = NSImage(named: weather.icon)
}
}
1
2
3
4
5
6
7
8

注意这里使用dispatch_async调用UI线程来刷新UI,因为后面调用此函数的数据来源于网络请求子线程。
StatusMenuController添加weatherView outlet
class StatusMenuController: NSObject {
@IBOutlet weak var statusMenu: NSMenu!
@IBOutlet weak var weatherView: WeatherView!
var weatherMenuItem: NSMenuItem!
...
1
2
3
4
5
子菜单Weather绑定到视图
weatherMenuItem = statusMenu.itemWithTitle("Weather")
weatherMenuItem.view = weatherView
1
2
update中:
func updateWeather() {
weatherAPI.fetchWeather("Seattle, WA") { weather in
self.weatherView.update(weather)
}
}
1
2
3
4
5

运行一下。


十一、添加天气图片

先添加天气素材到Xcode,天气素材可以在http://openweathermap.org/weather-conditions 这里找到。这里我已经提供了一份icon
zip, 解压后放Xcode。 



WeatherAPI.swift的Weather struct中,添加 
var
icon: String


在weatherFromJSONData中:

let weather = Weather(
city: json["name"] as! String,
currentTemp: mainDict["temp"] as! Float,
conditions: weatherDict["main"] as! String,
icon: weatherDict["icon"] as! String
)
1
2
3
4
5
6
在weatherFromJSONData:
let weather = Weather(
city: json["name"] as! String,
currentTemp: mainDict["temp"] as! Float,
conditions: weatherDict["main"] as! String,
icon: weatherDict["icon"] as! String
)
1
2
3
4
5
6
在WeatherView的update中:
imageView.image = NSImage(named: weather.icon)
1

运行一下,Pretty!




十二、添加设置

在MainMenu.xib MenuItem中,添加一个Menu Item命名为“Preferences…” 

并绑定action,命名为“preferencesClicked”
添加NSWindowController命名为PreferencesWindow.swift New ⟶ File ⟶ OS X Source ⟶ Cocoa Class , 勾选同时创建XIB.在XIB中添加Label和Text Field。效果如下: 



Outlet cityTextField到PreferencesWindow.swift
在PreferencesWindow.swift中添加:
override var windowNibName : String! {
return "PreferencesWindow"
}
1
2
3
windowDidLoad()中修改:
self.window?.center()
self.window?.makeKeyAndOrderFront(nil)
NSApp.activateIgnoringOtherApps(true)
1
2
3
最终PreferencesWindow.swift如下:
import Cocoa

class PreferencesWindow: NSWindowController {
@IBOutlet weak var cityTextField: NSTextField!

override var windowNibName : String! { return "PreferencesWindow" }

override func windowDidLoad() {
super.windowDidLoad()

self.window?.center() self.window?.makeKeyAndOrderFront(nil) NSApp.activateIgnoringOtherApps(true)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

StatusMenuController.swift中添加preferencesWindow 
var preferencesWindow: PreferencesWindow!


awakeFromNib中,注意在updateWeather()之前: 
preferencesWindow = PreferencesWindow()


preferencesClicked中: 
preferencesWindow.showWindow(nil)


下面为 preferences window 添加NSWindowDelegate,刷新视图。 
class PreferencesWindow: NSWindowController,
NSWindowDelegate {
 

并增加

func windowWillClose(notification: NSNotification) {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setValue(cityTextField.stringValue, forKey: "city")
}
1
2
3
4

增加协议:
protocol PreferencesWindowDelegate {
func preferencesDidUpdate()
}
1
2
3

增加delegate:
var delegate: PreferencesWindowDelegate?
1

在windowWillClose最下面调用
delegate?.preferencesDidUpdate()
1
回到StatusMenuController中,添加PreferencesWindowDelegate
class StatusMenuController: NSObject, PreferencesWindowDelegate {
1

实现代理:
func preferencesDidUpdate() {
updateWeather()
}
1
2
3

awakeFromNib中:
preferencesWindow = PreferencesWindow()
preferencesWindow.delegate = self
1
2

在StatusMenuController中增加默认城市 

let DEFAULT_CITY = “Seattle, WA”

修改updateWeather

func updateWeather() {
let defaults = NSUserDefaults.standardUserDefaults()
let city = defaults.stringForKey("city") ?? DEFAULT_CITY
weatherAPI.fetchWeather(city) { weather in
self.weatherView.update(weather)
}
}
1
2
3
4
5
6
7
咱们也可以在PreferencesWindow.swift windowDidLoad中设置city默认值
let defaults = NSUserDefaults.standardUserDefaults()
let city = defaults.stringForKey("city") ?? DEFAULT_CITY
cityTextField.stringValue = city
1
2
3

运行。一切OK。

其他: 

- 你也可以试试使用
NSRunLoop.mainRunLoop().addTimer(refreshTimer!,
forMode: NSRunLoopCommonModes)
 来定时updateWeather. 

- 试试点击天气后跳转到天气中心 
NSWorkspace.sharedWorkspace().openURL(url:
NSURL))
 

- 完整工程: WeatherBar
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐