
下面开始介绍如何使用Swift开发一个Mac Menu bar(Status bar) App。通过做一个简单的天气App。天气数据来源于OpenWeatherMap。
完成后的效果如下:
01
@H_502_39@开始建立工程
@H_502_39@@H_502_39@@H_502_39@打开Xcode,Create a New Project or file - New - Project - Application - Cocoa Application ( OS X 这一栏)。点击下一步。
02
@H_502_39@开始代码工作
@H_502_39@@H_502_39@@H_502_39@1.打开MainMenu.xib,删除默认的windows和menu菜单。因为我们是状态栏app,不需要菜单栏,不需要主窗口。
2.添加一个Menu菜单
删除其中默认的2个子菜单选项,仅保留1个。并将保留的这个改名为“Quit”。
3.打开双视图绑定Outlet
将Menu Outlet到AppDelegate,命名为statusMenu
将子菜单Quit绑定Action到AppDelegate,命名为quitClicked
@H_301_166@
你可以删除 @IBOutlet weak var window: NSWindow! ,这个app中用不上。
4.代码
在AppDelegate.swift中statusMenu下方添加
let statusItem = Nsstatusbar.systemStatusbar().statusItemWithLength(NSVariableStatusItemLength)
applicationDIDFinishLaunching函数中添加
statusItem.Title = "Weatherbar"
statusItem.menu = statusMenu
在quitClicked中添加
NSApplication.sharedApplication().terminate(self)
此时你的代码应该如下
运行,你可以看到一个状态栏了。
03
@H_502_39@进阶一步,让App变得更好
@H_502_39@@H_502_39@@H_502_39@你应该注意到了,当你运行后,底部Dock栏里出现了一个App启动的Icon。但实际上我们也不需要这个启动icon,打开Info,添加 “Application is agent (UIElement)”为YES。
运行一下,不会出现dock启动icon了。
04
@H_502_39@添加状态栏Icon
@H_502_39@@H_502_39@@H_502_39@状态栏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
运行一下,你应该看到状态栏icon了。
05
@H_502_39@重构下代码
@H_502_39@@H_502_39@@H_502_39@如果我们进一步写下去,你会发现大量代码在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)
}
}
还原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
}
}
注意,因为删除了AppDelegate中的Outlet注册,所以你需要重新连Outlet,但在这之前我们需要先做一件事。(你可以试试连接StatusMenuController中的Outlet,看看会怎么样?)
打开MainMenu.xib,添加一个Object。
将该Object的Class指定为StatusMenuController
重建Outlet到StatusMenuController,注意删除之前连接到AppDelegate的Outlet
当MainMenu.xib被初始化的时候,StatusMenuController下的awakeFromNib将会被执行,所以我们在里面做初始化工作。
运行一下,保证你全部正常工作了。
06
@H_502_39@天气API
@H_502_39@@H_502_39@@H_502_39@我们使用 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()
}
}
添加一个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)
}
}
07
@H_502_39@解析JsON
@H_502_39@@H_502_39@@H_502_39@你可以使用 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
}
在WeatherAPI.swift添加天气结构体用于解析son
struct Weather {
var city: String
var currentTemp: float
var conditions: String
}
解析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
}
修改fetchWeather函数去调用weatherFromJsONData
let task = session.dataTaskWithURL(url!) { data,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 %@",NShttpURLResponse.localizedStringForStatusCode(httpResponse.statusCode))
}
}
}
如果此时你运行,你会收到
2016-07-28 11:25:08.457 Weatherbar[49688:1998824] Optional(Weatherbar.Weather(city: "Seattle",currentTemp: 51.6,conditions: "Clouds"))
给Weather结构体添加一个description
struct Weather: customstringconvertible {
var city: String
var currentTemp: float
var conditions: String
var description: String {
return "\(city): \(currentTemp)F and \(conditions)"
}
}
再运行试试。
08
@H_502_39@Weather用到Controller中
@H_502_39@@H_502_39@@H_502_39@在 WeatherAPI.swift中增加delegate协议
protocol WeatherAPIDelegate {
func weatherDIDUpdate(weather: Weather)
}
声明var delegate: WeatherAPIDelegate?
添加初始化
init(delegate: WeatherAPIDelegate) {
self.delegate = delegate
}
修改fetchWeather
let task = session.dataTaskWithURL(url!) { data,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 %@",NShttpURLResponse.localizedStringForStatusCode(httpResponse.statusCode))
}
}
}
StatusMenuController添加WeatherAPIDelegate
class StatusMenuController: NSObject,WeatherAPIDelegate {
...
var weatherAPI: WeatherAPI!
overrIDe func awakeFromNib() {
...
weatherAPI = WeatherAPI(delegate: self)
updateWeather()
}
...
func weatherDIDUpdate(weather: Weather) {
NSLog(weather.description)
}
...
Callback实现,修改WeatherAPI.swift中fetchWeather:
func fetchWeather(query: String,success: (Weather) -> VoID) {
修改fetchWeather内容
let task = session.dataTaskWithURL(url!) { data,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 %@",NShttpURLResponse.localizedStringForStatusCode(httpResponse.statusCode))
}
}
}
在controller中
func updateWeather() {
weatherAPI.fetchWeather("Seattle,WA") { weather in
NSLog(weather.description)
}
}
运行一下,确保都正常。
09
@H_502_39@显示天气
@H_502_39@@H_502_39@@H_502_39@在MainMenu.xib中添加子菜单 “Weather”(你可以添加2个Separator Menu Item用于子菜单分割线)
在updateWeather中,替换NSLog:
if let weatherMenuItem = self.statusMenu.itemWithTitle("Weather") {
weatherMenuItem.Title = weather.description
}
运行一下,看看天气是不是显示出来了。
10
@H_502_39@创建一个天气视图
@H_502_39@@H_502_39@@H_502_39@打开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!
}
并添加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)
}
}
注意这里使用dispatch_async调用UI线程来刷新UI,因为后面调用此函数的数据来源于网络请求子线程。
StatusMenuController添加weatherVIEw outlet
class StatusMenuController: NSObject {
@IBOutlet weak var statusMenu: NSMenu!
@IBOutlet weak var weatherVIEw: WeatherVIEw!
var weatherMenuItem: NSMenuItem!
...
子菜单Weather绑定到视图
weatherMenuItem = statusMenu.itemWithTitle("Weather")
weatherMenuItem.vIEw = weatherVIEw
update中:
func updateWeather() {
weatherAPI.fetchWeather("Seattle,WA") { weather in
self.weatherVIEw.update(weather)
}
}
运行一下。
11
@H_502_39@添加天气图片
@H_502_39@@H_502_39@@H_502_39@先添加天气素材到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
)
在weatherFromJsONData:
let weather = Weather(
city: Json["name"] as! String,
icon: weatherDict["icon"] as! String
)
在WeatherVIEw的update中:
imageVIEw.image = NSImage(named: weather.icon)
运行一下,Pretty!
12
@H_502_39@添加设置
@H_502_39@@H_502_39@@H_502_39@在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"
}
windowDIDLoad()中修改:
self.window?.center()
self.window?.makeKeyAndOrderFront(nil)
NSApp.activateIgnoringOtherApps(true)
最终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)
}
}
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")
}
增加协议:
protocol PreferencesWindowDelegate {
func preferencesDIDUpdate()
}
增加delegate:
var delegate: PreferencesWindowDelegate?
在windowWillClose最下面调用
delegate?.preferencesDIDUpdate()
回到StatusMenuController中,添加PreferencesWindowDelegate
class StatusMenuController: NSObject,PreferencesWindowDelegate {
实现代理:
func preferencesDIDUpdate() {
updateWeather()
}
awakeFromNib中:
preferencesWindow = PreferencesWindow()
preferencesWindow.delegate = self
在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)
}
}
咱们也可以在PreferencesWindow.swift windowDIDLoad中设置city默认值
let defaults = NSUserDefaults.standardUserDefaults()
let city = defaults.stringForKey("city") ?? DEFAulT_CITY
cityTextFIEld.stringValue = city
运行。一切OK。
其他:
- 你也可以试试使用NSRunLoop.mainRunLoop().addTimer(refreshTimer!,forMode: NSRunLoopCommonModes) 来定时updateWeather.
- 试试点击天气后跳转到天气中心 NSWorkspace.shareDWorkspace().openURL(url: NSURL))
- 完整工程: Weatherbar
参考
@H_235_3019@http://footle.org/Weatherbar/
2016 年 9 月 23-24 日,由 CSDN 和创新工场联合主办的“MDCC 2016 移动开发者大会• 中国”(Mobile Developer Conference China)将在北京• 国家会议中心召开,来自iOS、AndroID、跨平台开发、产品设计、VR开发、移动直播、人工智能、物联网、硬件开发、信息无障碍10个领域的技术专家将分享他们在各自行业的真知灼见。
从即日起至8月7日23:59,MDCC 2016移动开发者大会门票5折优惠。五人以上团购更有特惠,限量供应,预购从速。(票务详情链接)
总结
以上是内存溢出为你收集整理的使用Swift开发一个MacOS的菜单状态栏App全部内容,希望文章能够帮你解决使用Swift开发一个MacOS的菜单状态栏App所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)