新媒体管家

点击上方“程序员大咖”,选择“置顶公众号”

关键时刻,第一时间送达!

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

完成后的效果如下:

01

开始建立工程

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

02

开始代码工作

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

2.添加一个Menu菜单

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

3.打开双视图绑定Outlet

  • 将Menu Outlet到AppDelegate,命名为statusMenu

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

  • 你可以删除 @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

进阶一步,让App变得更好

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

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

04

添加状态栏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

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

05

重构下代码

如果我们进一步写下去,你会发现大量代码在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

天气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()

}

}

  • 添加一个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

解析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, 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))

}

}

}

如果此时你运行,你会收到

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

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

}

}

}

  • 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, 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))

}

}

}

  • 在controller中

func updateWeather() {

weatherAPI.fetchWeather("Seattle, WA") { weather in

NSLog(weather.description)

}

}

运行一下,确保都正常。

09

显示天气

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

在updateWeather中,替换NSLog:

if let weatherMenuItem = self.statusMenu.itemWithTitle("Weather") {

weatherMenuItem.title = weather.description

}

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

10

创建一个天气视图

打开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

添加天气图片

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

currentTemp: mainDict["temp"] as! Float,

conditions: weatherDict["main"] as! String,

icon: weatherDict["icon"] as! String

)

  • 在WeatherView的update中:

imageView.image = NSImage(named: weather.icon)

运行一下,Pretty!

12

添加设置

在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

参考

  • http://footle.org/WeatherBar/

  • 来自:CSDN-Cocos2der

  • http://blog.csdn.net/cocos2der/article/details/52054107

  • 程序员大咖整理发布,转载请联系作者获得授权

【点击成为Python大神】

使用Swift开发一个MacOS的菜单状态栏App相关推荐

  1. 【如何快速的开发一个完整的iOS直播app】(美颜篇)

    前言 在看这篇之前,如果您还不了解直播原理,请查看这篇文章如何快速的开发一个完整的iOS直播app(原理篇) 开发一款直播app,美颜功能是很重要的,如果没有美颜功能,可能分分钟钟掉粉千万,本篇主要讲 ...

  2. 【如何快速的开发一个完整的iOS直播app】(采集篇)

    前言 在看这篇之前,如果您还不了解直播原理,请查看这篇文章如何快速的开发一个完整的iOS直播app(原理篇) 开发一款直播app,首先需要采集主播的视频和音频,然后传入流媒体服务器,本篇主要讲解如何采 ...

  3. 如何快速的开发一个完整的iOS直播app(美颜篇)

    1.前言 在看这篇之前,如果您还不了解直播原理,请查看这篇文章如何快速的开发一个完整的iOS直播app(原理篇) 开发一款直播app,美颜功能是很重要的,如果没有美颜功能,可能分分钟钟掉粉千万,本篇主 ...

  4. 开发一个最简单的iOS App

    开发一个最简单的iOS App 大家好,我是孜孜不倦学习的Zhangbeihai. 上月底我组织了[组队学习]TensorFlow 入门课程(中文) ,截至目前有300多同学加入.主要就是 Tenso ...

  5. 【如何快速的开发一个简单的iOS直播app】(代码篇)

    开篇([如何快速的开发一个完整的iOS直播app](原理篇) ) 好久没写简书,因为好奇的我跑去学习直播了,今天就分享一下我的感慨. 目前为止直播还是比较热点的技术的,简书,git上有几篇阅读量和含金 ...

  6. 开发一个完整的iOS直播app必须技能

    今年,直播行业火了,当然也诞生了一大批网红,甚至明星也开始直播了,因此现在都要搞直播了!由于第一次接触,花了很多时间了解直播,目前整理了直播的原理(因为项目汇报的需要就整理了一下),现在只是展示一下从 ...

  7. (转)【如何快速的开发一个完整的iOS直播app】(原理篇)

    原文链接:https://www.jianshu.com/p/bd42bacbe4cc [如何快速的开发一个完整的iOS直播app](原理篇) [如何快速的开发一个完整的iOS直播app](原理篇) ...

  8. 如何快速的开发一个完整的iOS直播app(原理篇)

    本文转自袁峥Seemygo的博客分享.觉得很不错.特意粘来给大家分享. 1.一个完整直播app功能(来自落影loyinglin分享) 1.聊天 私聊.聊天室.点亮.推送.黑名单等; 2.礼物 普通礼物 ...

  9. 如何快速的开发一个完整的iOS直播app(播放篇)

    前言 在看这篇之前,如果您还不了解直播原理,请查看上篇文章如何快速的开发一个完整的iOS直播app(原理篇) 开发一款直播app,集成ijkplayer成功后,就算完成直播功能一半的工程了,只要有拉流 ...

最新文章

  1. 83. Remove Duplicates from Sorted List
  2. 用于部分迁移学习的加权对抗网络 | CVPR 2018论文解读
  3. solr 配置mysql数据源_solr data-config.xml配置文件的见解mysql数据源
  4. 如何在Java中检查对象是否为空?
  5. oracle之基本的sql_select语句全
  6. App-V轻量级应用程序虚拟化之三客户端测试
  7. [MySQL] 2059 - Authentication plugin ‘caching_sha2_password‘ cannot be loaded dlope
  8. PHP正则表达式实例汇总
  9. c语言 char *b,C语言:char*a,*b; *b*=a是什么意思? 如图所示
  10. 记忆的酒坛里,辛苦会酿成充实和幸福
  11. ue4缓存位置怎么改_[UE4]动态液体材质浅谈
  12. EEPROM与FLASH闪存到底有什么区别?
  13. CSS3实战 - 3d转换 - 超级立方体
  14. 为什么选择语音验证码?
  15. 斯坦福2021秋季·实用机器学习【中文】【合集】+1.1课程介绍
  16. siggraph_SIGGRAPH的Unity
  17. pytorch实现word_embedding(negative_sampling的skip-gram模型)
  18. 如何用手机播放电脑的声音
  19. 5-1链队入队出队操作
  20. 【HackTheBox】 meow

热门文章

  1. pcb二次钻孔_PCB钻孔的相关介绍
  2. shell脚本第三天
  3. oracurstat_性能诊断报告
  4. Springboot启动流程分析-1——脚手架工程和启动流程
  5. 十年蓄势,新基建风口,紫光云如何后发先至?
  6. downloading Olivetti faces from urllib.error.HTTPError: HTTP Error 403: Forbidden
  7. 如何低格台式计算机的硬盘,台式机机械硬盘之初始化,分区,格式化
  8. blocked for more than 120 seconds 及sysctl用法
  9. komodo安装教程
  10. “清理僵尸粉”惊天骗局:微信被黑客控制,聊天记录被黑客监视