教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友 微信微信
查看查看542 回复回复6 收藏收藏 分享淘帖 转播转播 分享分享 微信
查看: 542|回复: 6
收起左侧

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用

[复制链接]
游戏人生 发表于 2016-7-28 15:46:57 | 显示全部楼层 |阅读模式
快来登录
获取优质的苹果资讯内容
收藏热门的iOS等技术干货
拷贝下载Swift Demo源代码
订阅梳理好了的知识点专辑

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

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

完成后的效果如下:

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 - 敏捷大拇指 - 使用Swift开发一个MacOS的菜单状态栏App 1





1、开始建立工程

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

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 - 敏捷大拇指 - 使用Swift开发一个MacOS的菜单状态栏App 2





2、开始代码工作



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

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 - 敏捷大拇指 - 使用Swift开发一个MacOS的菜单状态栏App 3




2.2、添加一个Menu菜单

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 - 敏捷大拇指 - 使用Swift开发一个MacOS的菜单状态栏App 4


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



2.3、打开双视图绑定Outlet


2.3.1、将Menu Outlet到AppDelegate,命名为statusMenu

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 - 敏捷大拇指 - 使用Swift开发一个MacOS的菜单状态栏App 5



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

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 - 敏捷大拇指 - 使用Swift开发一个MacOS的菜单状态栏App 6



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



2.4、代码


2.4.1、在AppDelegate.swift中statusMenu下方添加

[Swift] 纯文本查看 复制代码
let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(NSVariableStatusItemLength)



2.4.2、applicationDidFinishLaunching函数中添加:

[Swift] 纯文本查看 复制代码
statusItem.title = "WeatherBar"
statusItem.menu = statusMenu



2.4.3、在quitClicked中添加:

[Swift] 纯文本查看 复制代码
NSApplication.sharedApplication().terminate(self)



2.4.4、此时你的代码应该如下:

[Swift] 纯文本查看 复制代码
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
    }

}


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




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

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

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 - 敏捷大拇指 - 使用Swift开发一个MacOS的菜单状态栏App 7


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




4、添加状态栏Icon

状态栏icon尺寸请使用18x18

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 - 敏捷大拇指 - 使用Swift开发一个MacOS的菜单状态栏App 21
  , 36x36(@2x)  

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 - 敏捷大拇指 - 使用Swift开发一个MacOS的菜单状态栏App 22
, 54x54(@3x),添加这1x和2x两张图到Assets.xcassets中。

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 - 敏捷大拇指 - 使用Swift开发一个MacOS的菜单状态栏App 8


在applicationDidFinishLaunching中,修改为如下:

[Swift] 纯文本查看 复制代码
let icon = NSImage(named: "statusIcon")
icon?.template = true // best for dark mode
statusItem.image = icon
statusItem.menu = statusMenu


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





5、重构下代码

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



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

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 - 敏捷大拇指 - 使用Swift开发一个MacOS的菜单状态栏App 9


代码如下:

[Swift] 纯文本查看 复制代码
// 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)
    }
}




5.2、还原AppDelegate,修改为如下:

[Swift] 纯文本查看 复制代码
// 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,看看会怎么样?)



5.3、打开MainMenu.xib,添加一个Object。

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 - 敏捷大拇指 - 使用Swift开发一个MacOS的菜单状态栏App 10


将该Object的Class指定为StatusMenuController

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 - 敏捷大拇指 - 使用Swift开发一个MacOS的菜单状态栏App 11


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

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 - 敏捷大拇指 - 使用Swift开发一个MacOS的菜单状态栏App 12


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

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




6、天气API

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



6.1、添加WeatherAPI.swift, File ⟶ New File ⟶ OS X Source ⟶ Swift File ⟶ WeatherAPI.swift,加入如下代码,并使用你自己的API Key。

[Swift] 纯文本查看 复制代码
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()
    }
}




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

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 - 敏捷大拇指 - 使用Swift开发一个MacOS的菜单状态栏App 13


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



6.3、开始使用WeatherAPI, 在StatusMenuController中let statusItem下面加入:

[Swift] 纯文本查看 复制代码
let weatherAPI = WeatherAPI()


在updateClicked中加入:

[Swift] 纯文本查看 复制代码
weatherAPI.fetchWeather("Seattle")


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

运行一下,然后点击Update菜单。你会收到一个json格式的天气数据。



6.4、我们再调整下StatusMenuController代码, 添加一个updateWeather函数,修改后如下:

[Swift] 纯文本查看 复制代码
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)
    }
}





7、解析JSON

你可以使用 SwiftyJSON,但本次我们先不使用第三方库。我们得到的天气数据如下:

[JavaScript] 纯文本查看 复制代码
{
    "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
}




7.1、在WeatherAPI.swift添加天气结构体用于解析JSON:

[Swift] 纯文本查看 复制代码
struct Weather {
    var city: String
    var currentTemp: Float
    var conditions: String
}




7.2、解析JSON

[Swift] 纯文本查看 复制代码
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
    }




7.3、修改fetchWeather函数去调用weatherFromJSONData

[Swift] 纯文本查看 复制代码
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))
        }
    }
}


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

[Swift] 纯文本查看 复制代码
2016-07-28 11:25:08.457 WeatherBar[49688:1998824] Optional(WeatherBar.Weather(city: "Seattle", currentTemp: 51.6, conditions: "Clouds"))




7.4、给Weather结构体添加一个description

[Swift] 纯文本查看 复制代码
struct Weather: CustomStringConvertible {
    var city: String
    var currentTemp: Float
    var conditions: String

    var description: String {
        return "\(city): \(currentTemp)F and \(conditions)"
    }
}


再运行试试。




8、Weather用到Controller中



8.1、在 WeatherAPI.swift中增加delegate协议

[Swift] 纯文本查看 复制代码
protocol WeatherAPIDelegate {
    func weatherDidUpdate(weather: Weather)
}




8.2、声明

[Swift] 纯文本查看 复制代码
var delegate: WeatherAPIDelegate?




8.3、添加初始化

[Swift] 纯文本查看 复制代码
init(delegate: WeatherAPIDelegate) {
    self.delegate = delegate
}




8.4、修改fetchWeather

[Swift] 纯文本查看 复制代码
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))
        }
    }
}




8.5、StatusMenuController添加WeatherAPIDelegate

[Swift] 纯文本查看 复制代码
class StatusMenuController: NSObject, WeatherAPIDelegate {
...
  var weatherAPI: WeatherAPI!

  override func awakeFromNib() {
    ...
    weatherAPI = WeatherAPI(delegate: self)
    updateWeather()
  }
  ...
  func weatherDidUpdate(weather: Weather) {
    NSLog(weather.description)
  }
  ...




8.6、Callback实现,修改WeatherAPI.swift中fetchWeather:

[Swift] 纯文本查看 复制代码
func fetchWeather(query: String, success: (Weather) -> Void) {


修改fetchWeather内容

[Swift] 纯文本查看 复制代码
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))
        }
    }
}




8.7、在controller中

[Swift] 纯文本查看 复制代码
func updateWeather() {
    weatherAPI.fetchWeather("Seattle, WA") { weather in
        NSLog(weather.description)
    }
}


运行一下,确保都正常。




9、显示天气

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

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 - 敏捷大拇指 - 使用Swift开发一个MacOS的菜单状态栏App 14


在updateWeather中,替换NSLog:

[Swift] 纯文本查看 复制代码
if let weatherMenuItem = self.statusMenu.itemWithTitle("Weather") {
    weatherMenuItem.title = weather.description
}


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




10、创建一个天气视图

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



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

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 - 敏捷大拇指 - 使用Swift开发一个MacOS的菜单状态栏App 15




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

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 - 敏捷大拇指 - 使用Swift开发一个MacOS的菜单状态栏App 16




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

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

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 - 敏捷大拇指 - 使用Swift开发一个MacOS的菜单状态栏App 17




10.4、绑定WeatherView Outlet:

[Swift] 纯文本查看 复制代码
import Cocoa

class WeatherView: NSView {
    @IBOutlet weak var imageView: NSImageView!
    @IBOutlet weak var cityTextField: NSTextField!
    @IBOutlet weak var currentConditionsTextField: NSTextField!
}


并添加update:

[Swift] 纯文本查看 复制代码
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,因为后面调用此函数的数据来源于网络请求子线程。



10.5、StatusMenuController添加weatherView outlet

[Swift] 纯文本查看 复制代码
class StatusMenuController: NSObject {
    @IBOutlet weak var statusMenu: NSMenu!
    @IBOutlet weak var weatherView: WeatherView!
    var weatherMenuItem: NSMenuItem!
    ...




10.6、子菜单Weather绑定到视图

[Swift] 纯文本查看 复制代码
weatherMenuItem = statusMenu.itemWithTitle("Weather")
weatherMenuItem.view = weatherView




10.7、update中:

[Swift] 纯文本查看 复制代码
func updateWeather() {
    weatherAPI.fetchWeather("Seattle, WA") { weather in
        self.weatherView.update(weather)
    }
}


运行一下。




11、添加天气图片

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

教你使用Swift开发一个简单的MacOS上的菜单状态栏App天气应用 weather-icons.zip.zip (56.01 KB, 下载次数: 0, 售价: 10 金钱)

都看到这里了,就把这篇资料推荐给您的好朋友吧,让他们也感受一下。

回帖是一种美德,也是对楼主发帖的尊重和支持。

*声明:敏捷大拇指是全球最大的Swift开发者社区、苹果粉丝家园、智能移动门户,所载内容仅限于传递更多最新信息,并不意味赞同其观点或证实其描述;内容仅供参考,并非绝对正确的建议。本站不对上述信息的真实性、合法性、完整性做出保证;转载请注明来源并加上本站链接,敏捷大拇指将保留所有法律权益。如有疑问或建议,邮件至marketing@swifthumb.com

*联系:微信公众平台:“swifthumb” / 腾讯微博:@swifthumb / 新浪微博:@swifthumb / 官方QQ一群:343549891(满) / 官方QQ二群:245285613 ,需要报上用户名才会被同意进群,请先注册敏捷大拇指

嗯,不错!期待更多好内容,支持一把:
支持敏捷大拇指,用支付宝支付10.24元 支持敏捷大拇指,用微信支付10.24元

评分

参与人数 1金钱 +10 贡献 +10 专家分 +10 收起 理由
Anewczs + 10 + 10 + 10 32个赞!专家给力!

查看全部评分

growthhacker 发表于 2016-7-28 16:49:37 | 显示全部楼层
一步步教得好详细啊,谢谢楼主!
tuorsrui 发表于 2016-7-29 09:35:02 | 显示全部楼层
给mac机写东西,头回见,学习学习
 楼主| 游戏人生 发表于 2016-7-31 05:02:18 | 显示全部楼层
tuorsrui 发表于 2016-7-29 09:35
给mac机写东西,头回见,学习学习

tvOS上的也没写过吧
h5lover 发表于 2016-8-30 22:44:20 | 显示全部楼层
好详细的教程!!
攻城狮 发表于 2016-9-1 15:49:35 | 显示全部楼层
太详细了!!!32个赞!
智能科技 发表于 2016-9-1 20:47:55 | 显示全部楼层
楼主功底好深啊!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

做任务,领红包。
我要发帖

分享扩散

都看到这里了,就把这资料推荐给您的好朋友吧,让他们也感受一下。
您的每一位朋友访问此永久链接后,您都将获得相应的金钱积分奖励
热门推荐

合作伙伴

Swift小苹果

  • 北京治世天下科技有限公司
  • ©2014-2016 敏捷大拇指
  • 京ICP备14029482号
  • Powered by Discuz! X3.1 Licensed
  • swifthumb Wechat Code
  •   
快速回复 返回顶部 返回列表