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

使用Swift开发一个MacOS的菜单状态栏App,第1张

概述猴子原创,欢迎转载。转载请注明: 转载自Cocos2Der-CSDN,谢谢! 原文地址: http://www.voidcn.com/article/p-gosdnwni-vc.html 这两天突然想看看OSX下的App开发,看了几篇文章。下面这一篇我觉得入门是非常好的。我仅转述为中文,并非原文翻译。原文地址:http://footle.org/WeatherBar/ 下面开始介绍如何使用Swif

猴子原创,欢迎转载。转载请注明: 转载自Cocos2Der-CSDN,谢谢!
原文地址: http://www.jb51.cc/article/p-gosdnwni-vc.html

这两天突然想看看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)

applicationDIDFinishLaunching函数中添加:

statusItem.Title = "Weatherbar"statusItem.menu = statusMenu

在quitClicked中添加:

NSApplication.sharedApplication().terminate(self)

此时你的代码应该如下:

import Cocoa@NSApplicationMainclass 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 } }

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

三、进阶一步,让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 modestatusItem.image = iconstatusItem.menu = statusMenu

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

五、重构下代码

如果我们进一步写下去,你会发现大量代码在AppDelegate中,我们不希望这样。下面我们为Menu创建一个Controller来管理。

新建一个NSObject的StatusMenuController.swift,file ⟶ New file ⟶ OS X Source ⟶ Cocoa Class ⟶ Next

代码如下:

// StatusMenuController.swiftimport Cocoaclass 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.swiftimport Cocoa@NSApplicationMainclass 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将会被执行,所以我们在里面做初始化工作。

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

六、天气API

我们使用 OpenWeatherMap的天气数据,所以你得注册一个账号,获取到免费的API Key。

添加WeatherAPI.swift, file ⟶ New file ⟶ OS X Source ⟶ Swift file ⟶ WeatherAPI.swift,加入如下代码,并使用你自己的API Key。
import Foundationclass 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 Cocoaclass 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)    }}
七、解析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 }
在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 %@",httpResponse.statusCode,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)"    }}

再运行试试。

八、Weather用到Controller中 在 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)    }}

运行一下,确保都正常。

九、显示天气

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

在updateWeather中,替换NSLog:

if let weatherMenuItem = self.statusMenu.itemWithTitle("Weather") {    weatherMenuItem.Title = weather.description}

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

十、创建一个天气视图

打开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 Cocoaclass 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)    }}

运行一下。

十一、添加天气图片

先添加天气素材到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!

十二、添加设置

在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 Cocoaclass 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_CITYcityTextFIEld.stringValue = city

运行。一切OK。

其他:
- 你也可以试试使用NSRunLoop.mainRunLoop().addTimer(refreshTimer!,forMode: NSRunLoopCommonModes) 来定时updateWeather.
- 试试点击天气后跳转到天气中心 NSWorkspace.shareDWorkspace().openURL(url: NSURL))
- 完整工程: WeatherBar

总结

以上是内存溢出为你收集整理的使用Swift开发一个MacOS的菜单状态栏App全部内容,希望文章能够帮你解决使用Swift开发一个MacOS的菜单状态栏App所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/web/1090491.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-27
下一篇 2022-05-27

发表评论

登录后才能评论

评论列表(0条)

保存