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

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

概述下面开始介绍如何使用Swift开发一个Mac Menu Bar(Status Bar) App。通过做一个简单的天气App。天气数据来源于OpenWeatherMap。 完成后的效果如下:  01 开始建立工程 打开Xcode,Create a New Project or File -  New - Project -  Application - Cocoa Application ( OS


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


完成后的效果如下: 




@H_403_33@

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 这一栏)。点击下一步。 




@H_403_33@

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)


此时你的代码应该如下




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


@H_403_33@

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了。


@H_403_33@

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了。


@H_403_33@

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将会被执行,所以我们在里面做初始化工作。


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


@H_403_33@

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)

    }

}


@H_403_33@

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)"

    }

}


再运行试试。


@H_403_33@

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)

    }

}


运行一下,确保都正常。


@H_403_33@

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

}


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


@H_403_33@

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)

    }

}


运行一下。


@H_403_33@

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!




@H_403_33@

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所遇到的程序开发问题。

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

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

原文地址:https://54852.com/web/1025662.html

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

发表评论

登录后才能评论

评论列表(0条)

    保存