为你的MacOS App添加开机自启动(Swift)

2016年8月3日 没有评论

猴子原创,欢迎转载。转载请注明: 转载自Cocos2Der-CSDN,谢谢!
原文地址: http://blog.csdn.net/cocos2der/article/details/52104828

关于Mac下如何给自己App添加开机自启动功能,你可以了解下Mac Developer Library中的说明。

There are two ways to add a login item: using the Service Management framework, and using a shared file list

Login items installed using the Service Management framework are not visible in System Preferences and can only be removed by the application that installed them.

Login items installed using a shared file list are visible in System Preferences; users have direct control over them. If you use this API, your login item can be disabled by the user, so any other application that communicates with it it should have reasonable fallback behavior in case the login item is disabled.

可以看出,Apple推荐了两种方式:Service Management framework 和 shared file list。

这两种方式有差别:

  • 使用Service Management framework 在系统的登录项中是不可见的。只有卸载App才能移除登录项

  • 使用 shared file list 在系统的登录项中是可见的。用户可以直接在面板上控制他们。(If you use this API, your login item can be disabled by the user, so any other application that communicates with it it should have reasonable fallback behavior in case the login item is disabled.) 原文还有一句大意是指这个API有隐患,所以在OS X 10.10系统上 API被大量Deprecated

下面我主要介绍的是使用Service Management framework 的方式添加开机自启动。

一、创建主工程

新建一个MainApp的osx工程App
为你的MacOS App添加开机自启动(Swift)

二、添加自动启动Target

  • 我们需要注册一个Helper Target App用来作为开机自启动我们的MainApp,点击Targets下面的加号.
    为你的MacOS App添加开机自启动(Swift)

  • 添加一个新的OS X application。取名为MainAppHelper
    为你的MacOS App添加开机自启动(Swift)

三、设置配置属性

  • 删除MainAppHelper中的windows,让它没有可展示的Window。
    为你的MacOS App添加开机自启动(Swift)

  • 设置MainAppHelper的Info中Application is background only为YES

  • 设置MainAppHelper中Build Setting下skip install为YES
    为你的MacOS App添加开机自启动(Swift)

  • 在MainApp中添加CopyFile到Contents/Library/LoginItems
    为你的MacOS App添加开机自启动(Swift)

  • 在MainApp中设置Build Setting 下Strip Debug Symbols During Copy为NO, 这个是默认的为No

  • 分别开启MainApp和MainAppHelper的App Sandbox
    为你的MacOS App添加开机自启动(Swift)

四、添加启动代码

  • 请根据你自己的实际情况添加startupAppWhenLogin函数到你MainApp主工程中,程序运行后根据情况调用开启或者关闭自启动, 注意其中的BundleID改为你自己的,以及主App的名称。如果不确定名称是否是自己的工程名,可以直接右键自己的ipa,显示包内容,看下里面的文件夹你就明白了。
func startupAppWhenLogin(startup: Bool) {
        // 这里请填写你自己的Heler BundleID
        let launcherAppIdentifier = "com.cocos2dev.MainApp.MainAppHelper"

        // 开始注册/取消启动项
        SMLoginItemSetEnabled(launcherAppIdentifier, startup)

        var startedAtLogin = false
        for app in NSWorkspace.sharedWorkspace().runningApplications {
            if app.bundleIdentifier == launcherAppIdentifier {
                startedAtLogin = true
            }
        }

        if startedAtLogin {            NSDistributedNotificationCenter.defaultCenter().postNotificationName("killhelper", object: NSBundle.mainBundle().bundleIdentifier!)
        }
    }
  • 在MainAppHelper中,修改AppDelegate为如下代码:

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(aNotification: NSNotification) {
        // Insert code here to initialize your application

        let mainAppIdentifier = "com.cocos2dev.MainApp"
        let running           = NSWorkspace.sharedWorkspace().runningApplications
        var alreadyRunning    = false

        for app in running {
            if app.bundleIdentifier == mainAppIdentifier {
                alreadyRunning = true
                break
            }
        }

        if !alreadyRunning {
            NSDistributedNotificationCenter.defaultCenter().addObserver(self, selector: "terminate", name: "killhelper", object: mainAppIdentifier)

            let path = NSBundle.mainBundle().bundlePath as NSString
            var components = path.pathComponents
            components.removeLast()
            components.removeLast()
            components.removeLast()
            components.append("MacOS")
            components.append("MainApp") //main app name

            let newPath = NSString.pathWithComponents(components)
            NSWorkspace.sharedWorkspace().launchApplication(newPath)
        } else {
            self.terminate()
        }
    }

    func applicationWillTerminate(aNotification: NSNotification) {
        // Insert code here to tear down your application
    }

    func terminate() {
        //      NSLog("I'll be back!")
        NSApp.terminate(nil)
    }

}

以上就是一个自启动App的做法。当然如果你上架Mac Store,建议你不要默认就开启自启动,放到设置中,让用户自己选择开启/关闭。

参考博客
Mac Developer Library
First OS X tutorial: How to launch an OS X app at login?
在SandBox沙盒下实现程序的开机启动

分类: 未分类 标签:

MacOS的菜单状态栏App添加饼型进度

2016年7月30日 没有评论

猴子原创,欢迎转载。转载请注明: 转载自Cocos2Der-CSDN,谢谢!
原文地址: http://blog.csdn.net/cocos2der/article/details/52075418

使用swift如何开发一个MacOS的状态栏App,上一篇已经讲了。里面我忘记提如何修改状态icon为饼型进度。比如App在处理什么事情的时候,可以添加进度状态提示用户。如下图所示:
MacOS的菜单状态栏App添加饼型进度

/// 显示状态栏菜单饼型进度
    private func showStatusItemProgress() {
        if let button = statusItem.button {
            // FIXME: it works, but obviously not good.
            button.subviews.removeAll()
        }
        if let button = statusItem.button {
            // FIXME: it works, but obviously not good.
            let frame = NSRect(x: 6, y: 2, width: 18, height: 18)
            let progressIndicator = NSProgressIndicator(frame: frame)
            progressIndicator.style = .SpinningStyle
            progressIndicator.indeterminate = false
            progressIndicator.minValue = 0
            progressIndicator.maxValue = 100
            progressIndicator.doubleValue = 0
            self.progressIndicator = progressIndicator

            // 当添加进度后,发现状态栏frame大小错误了,没找到解决办法,但是填充一个图片可以解决这个尺寸错误问题
            statusItem.image = NSImage(named: "EmptyIcon")
            statusItem.image?.template = true
            button.addSubview(progressIndicator)
        }
    }

请注意,
– 我们是把一个NSProgressIndicator添加到了NSStatusItem的button视图上,不要添加到view视图上,如果添加到view上,view会挡住状态栏点击事件,这样NSStatusItem会不能响应用户点击了。(当然,如果你app恰好不需要用户点击,可以这样做)
– 你应该留意到了,我在NSStatusItem的image里放了一张透明图片,因为这了是为了解决它frame大小错误的问题

当然,如果你知道如何解决的话,可以告诉我下。哈哈。

分类: 未分类 标签:

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

2016年7月28日 没有评论

猴子原创,欢迎转载。转载请注明: 转载自Cocos2Der-CSDN,谢谢!
原文地址: http://blog.csdn.net/cocos2der/article/details/52054107

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

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

完成后的效果如下:
使用Swift开发一个MacOS的菜单状态栏App

一、开始建立工程

打开Xcode,Create a New Project or File ⟶ New ⟶ Project ⟶ Application ⟶ Cocoa Application ( OS X 这一栏)。点击下一步。
使用Swift开发一个MacOS的菜单状态栏App

二、开始代码工作

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

  2. 添加一个Menu菜单
    使用Swift开发一个MacOS的菜单状态栏App
    删除其中默认的2个子菜单选项,仅保留1个。并将保留的这个改名为“Quit”。

  3. 打开双视图绑定Outlet

    • 将Menu Outlet到AppDelegate,命名为statusMenu
      使用Swift开发一个MacOS的菜单状态栏App

    • 将子菜单Quit绑定Action到AppDelegate,命名为quitClicked
      使用Swift开发一个MacOS的菜单状态栏App

    • 你可以删除 @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)
    • 此时你的代码应该如下:

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
    }

}

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

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

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

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

四、添加状态栏Icon

状态栏icon尺寸请使用18×18使用Swift开发一个MacOS的菜单状态栏App, 36×36(@2x)使用Swift开发一个MacOS的菜单状态栏App, 54×54(@3x),添加这1x和2x两张图到Assets.xcassets中。
使用Swift开发一个MacOS的菜单状态栏App

在applicationDidFinishLaunching中,修改为如下:

let icon = NSImage(named: "statusIcon")
icon?.template = true // best for dark mode
statusItem.image = icon
statusItem.menu = statusMenu

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

五、重构下代码

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

  • 新建一个NSObject的StatusMenuController.swift, File ⟶ New File ⟶ OS X Source ⟶ Cocoa Class ⟶ Next
    使用Swift开发一个MacOS的菜单状态栏App

代码如下:

// 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。
    使用Swift开发一个MacOS的菜单状态栏App
    将该Object的Class指定为StatusMenuController
    使用Swift开发一个MacOS的菜单状态栏App
    重建Outlet到StatusMenuController,注意删除之前连接到AppDelegate的Outlet
    使用Swift开发一个MacOS的菜单状态栏App

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

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

六、天气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。
    使用Swift开发一个MacOS的菜单状态栏App
    绑定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)
    }
}

七、解析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)"
    }
}

再运行试试。

八、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)
    }
}

运行一下,确保都正常。

九、显示天气

在MainMenu.xib中添加子菜单 “Weather”(你可以添加2个Separator Menu Item用于子菜单分割线)
使用Swift开发一个MacOS的菜单状态栏App

在updateWeather中,替换NSLog:

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

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

十、创建一个天气视图

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

  • 拖一个Image View到Custom View中,设置ImageView宽高度为50。
    使用Swift开发一个MacOS的菜单状态栏App

  • 拖两个Label进来,分别为City和Temperature
    使用Swift开发一个MacOS的菜单状态栏App

  • 创建一个名为WeatherView的NSView,New File ⟶ OS X Source ⟶ Cocoa Class
    在MainMenu.xib中,将Custom View的Class指定为WeatherView
    使用Swift开发一个MacOS的菜单状态栏App

  • 绑定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)
    }
}

运行一下。

十一、添加天气图片

先添加天气素材到Xcode,天气素材可以在http://openweathermap.org/weather-conditions 这里找到。这里我已经提供了一份icon zip, 解压后放Xcode。
使用Swift开发一个MacOS的菜单状态栏App

  • 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!

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

十二、添加设置

在MainMenu.xib MenuItem中,添加一个Menu Item命名为“Preferences…”
并绑定action,命名为“preferencesClicked”

  • 添加NSWindowController命名为PreferencesWindow.swift New ⟶ File ⟶ OS X Source ⟶ Cocoa Class , 勾选同时创建XIB.在XIB中添加Label和Text Field。效果如下:
    使用Swift开发一个MacOS的菜单状态栏App

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

分类: 未分类 标签:

Swift中实现Observable机制

2016年7月15日 没有评论

猴子原创,欢迎转载。转载请注明: 转载自Cocos2Der-CSDN,谢谢!
原文地址: http://blog.csdn.net/cocos2der/article/details/51917539

今天给别人讲个Observable的实现和使用场景,结合Observable-Swift github: https://github.com/slazyk/Observable-Swift 讲了半天貌似还没有特别明白,故写了个简易的实现,讲述了下Observable属性监控机制。

//: Playground - noun: a place where people can play

import UIKit
import Foundation

// MARK: - Observable
class Observable<T> {
    // 定义block结构
    typealias Observer = T -> Void

    // 申明一个block,用于数据改变的执行
    private var observer: Observer?

    // 数据发生变更,则通过observer告知
    var value: T {
        didSet {
            observer?(value)
        }
    }

    init(_ v: T) {
        value = v
    }

    func observe(observer: Observer?) {
        self.observer = observer
        observer?(value)
    }
}

// MARK: - People
struct PeopleModel {
    let firstName: Observable<String>
    let lastName: Observable<String>

    init(firstName: String, lastName: String) {
        self.firstName = Observable(firstName)
        self.lastName = Observable(lastName)
    }
}

// MARK: - Test

// test1
let people = PeopleModel(firstName: "sunny", lastName: "liu")
people.firstName.observe {
    newValue in
    print("firstName changed: /(newValue)")
}
people.lastName.observe {
    print("lastName changed: /($0)")
}
people.firstName.value = "sunny2"
people.lastName.value = "liu2"

// test2
class House {
    let lableHouseName =  UILabel()

    init() {

    }

    var people: PeopleModel? {
        didSet {
            people?.firstName.observe{
                [unowned self] in
                self.lableHouseName.text = $0
            }
        }
    }
}

这样貌似容易理解了,O(∩_∩)O哈哈~

分类: 未分类 标签:

[置顶] 继续加油~,用这一款工具帮助更多的Cocos3D团队。

2016年7月11日 没有评论

继续加油~,用这一款工具帮助更多的Cocos3D团队!

              花了一周多时间,完成了一款Cocos Mesh Viewer ,截止现在,终于算有一个凑合的功能版本可以用。

              下载地址:点击打开链接

              已经支持的功能:

             (1)打开任意FBX 转换为C3B文件。

             (2)打开C3B文件,并显示模型的相关信息(顶点数,面数,子模型数量,包围盒大小,骨骼数,当前帧数)。

             (3)显示线框模式,包围盒,骨骼树,隐模型。

             (4)显示骨骼清单,并可以在骨骼上绑定模型进行测试。

             (5)显示子模型清单,并可以从其它C3B中更换子模型,进行换装测试。

             (6)显示纹理清单,并可以提示未找到的贴图,点击贴图名称,显示贴图。

             (7)调节模型大小。

             (8)调节动作播放速度。

              

             对于使用Cocos开发3D项目,我觉得首先一定要有一个功能强大,而使用方便的Mesh Viewer。

            

             怎么样?有没有想试试!

  [置顶]        继续加油~,用这一款工具帮助更多的Cocos3D团队。

[置顶]        继续加油~,用这一款工具帮助更多的Cocos3D团队。

[置顶]        继续加油~,用这一款工具帮助更多的Cocos3D团队。

[置顶]        继续加油~,用这一款工具帮助更多的Cocos3D团队。

[置顶]        继续加油~,用这一款工具帮助更多的Cocos3D团队。

[置顶]        继续加油~,用这一款工具帮助更多的Cocos3D团队。

[置顶]        继续加油~,用这一款工具帮助更多的Cocos3D团队。

[置顶]        继续加油~,用这一款工具帮助更多的Cocos3D团队。

[置顶]        继续加油~,用这一款工具帮助更多的Cocos3D团队。

[置顶]        继续加油~,用这一款工具帮助更多的Cocos3D团队。

分类: 未分类 标签:

[置顶] Cocos开发VR菜鸟宝典 第八讲:为GearVR开发一款深海捕鱼VR游戏

2016年7月7日 没有评论

火云开发课堂开讲啦!

Cocos开发VR菜鸟宝典

        

                  本套课程分为基础班和高级班两种,基础班主要在CSDN上推出视频,学员自学为主。高级班在此基础上再进行项目实训(包含Cocos3D,Shader基础班和VR设备学习套装)

有兴趣参加高级班的可以加我QQ:285421210.


                 一不小心,就又到最后一节课了~,加油!!!

第八章:为GearVR开发一款深海捕鱼VR游戏


视频地址:http://edu.csdn.net/course/detail/2366/36776?auto_start=1


[置顶]        Cocos开发VR菜鸟宝典 第八讲:为GearVR开发一款深海捕鱼VR游戏

1.准备GearVR和三星手机

[置顶]        Cocos开发VR菜鸟宝典 第八讲:为GearVR开发一款深海捕鱼VR游戏

2.登录Oculus

[置顶]        Cocos开发VR菜鸟宝典 第八讲:为GearVR开发一款深海捕鱼VR游戏

[置顶]        Cocos开发VR菜鸟宝典 第八讲:为GearVR开发一款深海捕鱼VR游戏

3.下载ADB工具,打印手机的设备号,通过Oculus下载签名文件。

[置顶]        Cocos开发VR菜鸟宝典 第八讲:为GearVR开发一款深海捕鱼VR游戏

[置顶]        Cocos开发VR菜鸟宝典 第八讲:为GearVR开发一款深海捕鱼VR游戏


4.准备模型资源

[置顶]        Cocos开发VR菜鸟宝典 第八讲:为GearVR开发一款深海捕鱼VR游戏

5.工程讲解和运行

[置顶]        Cocos开发VR菜鸟宝典 第八讲:为GearVR开发一款深海捕鱼VR游戏



[置顶]        Cocos开发VR菜鸟宝典 第八讲:为GearVR开发一款深海捕鱼VR游戏


   

分类: 未分类 标签:

[置顶] 使用CocosMeshViewer来转换和观察Cocos模型

2016年7月5日 没有评论

                前言:使用Cocos来开发3D和VR的基础课程即将结束,为了更好的帮助Cocos程序员学习和掌握使用Cocos开发3D和VR的方法,红孩儿课堂特推出学习套餐:


[置顶]        使用CocosMeshViewer来转换和观察Cocos模型

有兴趣的同学可以到CSDN视频学院购买学习:http://edu.csdn.net/combo/detail/195

另外有参加高级班的可以加我 QQ:285421210.

 

             为了好上加好的帮助Cocos程序员开发项目,我们也会陆续放出各种工具来帮助大家开发哦!本节课程即为讲解一下我们为大家提供的免费的Cocos模型转换和观察器CocosMeshViewer.

         

         使用CocosMeshViewer来转换和观察Cocos模型


         我们在使用Cocos来开发3D项目时,经常会觉得控制台操作FBX -> C3B这个转换很麻烦,而且想看一下C3B文件是否正确也需要开启工程进行查看。能不能有更方便的方式来进行相关处理呢?


         当然!我们为大家提供了一款免费小工具CocosMeshViewer . 它可以方便的帮我们把FBX文件转换成C3B,并且进行查看。下面我们来学习下。


         首先我们可以到任意一个"红孩儿Cocos2dx交流群”去查看群共享(比如 44208467, 275220292等,找到CocosMeshViewer,然后下载下来,解压后打开,如图:

[置顶]        使用CocosMeshViewer来转换和观察Cocos模型  

        然后我们双击“CocosMeshViewer.exe".启动程序.

[置顶]        使用CocosMeshViewer来转换和观察Cocos模型


        可以看到,程序启动后,会显示一个网格,上面有一个按钮。

点击它,就可以在我们电脑上查找相应的FBX或C3B文件进行查看。

        首先,我们来查看一个res目录中的C3B文件。

[置顶]        使用CocosMeshViewer来转换和观察Cocos模型

     点击“打开”之后,模型就显示在网格中央了,也就是位置(0,0,0)点。

      我们可以按着键盘W键离近点观看。

[置顶]        使用CocosMeshViewer来转换和观察Cocos模型

          我们可以看到,在上方还有一些模型的信息进行显示,特别是可以通过滑块对模型进行缩放和动作速度调节,是不是很方便?

 

          当美术用3dsMax做好一个模型并导出成FBX后,转换为C3B是必须要做的工作,手动命令行方式非常麻烦,我们则可以直接在这里通过打开一个FBX来进行转换。下面,我们试着打开其它目录的一个FBX文件。

[置顶]        使用CocosMeshViewer来转换和观察Cocos模型


             打开FBX文件时,它会调用fbx-conv.exe来进行转换,并将转换后的C3B和纹理拷到res中。

      [置顶]        使用CocosMeshViewer来转换和观察Cocos模型

            同时显示在网格中央。

[置顶]        使用CocosMeshViewer来转换和观察Cocos模型


         好了,虽然这个工具很小,但是…它的确很方便,不是么?


           


分类: 未分类 标签:

监听手机截屏事件

2016年6月29日 没有评论

猴子原创,欢迎转载。转载请注明: 转载自Cocos2Der-CSDN,谢谢!
原文地址: http://blog.csdn.net/cocos2der/article/details/51780954

今天无意中在百度地图中截屏路线的时候,顶部出现提示我的截屏信息。这细节挺好的,省去我后面需要使用该截屏的繁琐步骤。刚好手头空闲会,我也写个玩玩。哈哈哈~~

截屏在iOS7以前是需要使用小技巧来获取用户截屏事件的,iOS7之后,apple开放了用户截屏通知事件,所以现在做起来还是挺方便的。

// This notification is posted after the user takes a screenshot (for example by pressing both the home and lock screen buttons)
@available(iOS 7.0, *)
public let UIApplicationUserDidTakeScreenshotNotification: String

注册通知

NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.userDidTakeScreenshot), name: UIApplicationUserDidTakeScreenshotNotification, object: nil)

代码实现(swift)

/// 用户截屏完毕
    func userDidTakeScreenshot() {
        // 当前屏幕的image
        // 注意:为什么不直接从相册读取截屏图像
        //(万一用户直接拒绝可权限你不跪了,何况截屏之后,用户可不知道你会提示,第一反应肯定拒绝读取相册的权限)
        let image = imageWithScreenshot()

        let imageView = UIImageView(frame: CGRect(x: 50, y: 50, width: 320, height: 640))
        imageView.image = image
        self.view.addSubview(imageView)
    }

    /// 获取当前屏幕图片
    func imageWithScreenshot() -> UIImage? {
        let imageData = dataWithScreenshotInPNGFormat()
        return UIImage(data: imageData)
    }

    /// 截取当前屏幕
    func dataWithScreenshotInPNGFormat() -> NSData {
        var imageSize = CGSizeZero
        let screenSize = UIScreen.mainScreen().bounds.size
        let orientation = UIApplication.sharedApplication().statusBarOrientation
        if UIInterfaceOrientationIsPortrait(orientation) {
            imageSize = screenSize
        } else {
            imageSize = CGSizeMake(screenSize.height, screenSize.width)
        }

        UIGraphicsBeginImageContextWithOptions(imageSize, false, 0)
        let context = UIGraphicsGetCurrentContext()

        for window in UIApplication.sharedApplication().windows {
            CGContextSaveGState(context)
            CGContextTranslateCTM(context, window.center.x, window.center.y)
            CGContextConcatCTM(context, window.transform)
            CGContextTranslateCTM(context, -window.bounds.size.width * window.layer.anchorPoint.x, -window.bounds.size.height * window.layer.anchorPoint.y)

            if orientation == UIInterfaceOrientation.LandscapeLeft {
                CGContextRotateCTM(context, CGFloat(M_PI_2))
                CGContextTranslateCTM(context, 0, -imageSize.width)
            } else if orientation == UIInterfaceOrientation.LandscapeRight {
                CGContextRotateCTM(context, -CGFloat(M_PI_2))
                CGContextTranslateCTM(context, -imageSize.height, 0)
            } else if (orientation == UIInterfaceOrientation.PortraitUpsideDown) {
                CGContextRotateCTM(context, CGFloat(M_PI))
                CGContextTranslateCTM(context, -imageSize.width, -imageSize.height)
            }
            if window.respondsToSelector(#selector(UIView.drawViewHierarchyInRect(_:afterScreenUpdates:))) {
                window.drawViewHierarchyInRect(window.bounds, afterScreenUpdates: true)
            } else {
                window.layer.renderInContext(context!)
            }
            CGContextRestoreGState(context);
        }
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return UIImagePNGRepresentation(image)!
    }

注意:为什么不直接从相册读取截屏图像?
万一用户直接拒绝可权限你不跪了,何况截屏之后,用户可不知道你会提示,第一反应肯定拒绝读取相册的权限。

分类: 未分类 标签:

Unity3d开发(十五) AudioClip 参数解析

2016年6月25日 没有评论
Unity3d开发(十五) AudioClip 参数解析



Audio Clip

AudioClip 面板有很多参数,设置起来容易出错,这里记录一下,做备忘。

Unity3d开发(十五) AudioClip 参数解析
  • Force To Mono: 将多声道的声音合并成单声道,声音文件大小直接干掉好多,手机必选。合并声道之后,勾选Normalize可以使声音听起来更优一些。
  • Load In Background: 在后台加载,这可以使得声音不阻塞主加载线程。它默认是关闭的,官方的说法是为了保证游戏运行时声音体验的一致性。我觉得如果加载不会引起运行时卡顿,那么相对于提升加载时间和减少加载数量的优势,还是值得将其勾选的。
  • Preload Audio Data: 在进入场景时预加载音效,如果不勾选那么直到第一次被使用时才加载。背景音乐估计妥妥不勾选了,UI音效我觉得倒是可以勾上,反正基本都是要加载的,就别占用运行时间了。
  • Load Type – Decompress On Load: 声音一旦被加载就会解压储存内存中。这可以提供更好的声音响应,但会占用内存,尤其是Vorbis编码的声音,因此比较适合短小的声音。
  • Load Type – Compressed In Memory: 声音在内存中以压缩的形式储存,等播放时再解压。这种方式有轻微的效率消耗,但节省了内存,因此适合Vorbis形式的大文件。这部分消耗可以在 Profiler中Audio面板的DSP CPU看到。
  • Load Type – Streaming: 播放时解码。这种方式占用内存最小,却增加了磁盘读写和解压。这部分消耗可以在 Profiler中Audio面板的Streaming CPU看到。基本上是大文件才会采用的设置。
  • Compression Format – PCM: 最高的质量,最大的文件
  • Compression Format – ADPCM: 一些包含噪音,且会被多次播放的的音频,可以采用这个格式,例如,脚步,打击,武器等。它的比PCM压缩了3.5倍,CPU消耗却比Vorbis小,是高频小声音的最佳选择。
  • Compression Format – Vorbis: 压缩的更小的文件,但质量就不太过关了。压缩率可以在Quality中配置,可以边听边选,最后确定一个合适的压缩率。
  • Compression Format – Quality: 压缩比率,只对Vorbis类型有效果。最终文件大小在Inspector中可以看到。
  • Sample Rate Setting – Preserve Sample Rate: 先前默认的值。
  • Sample Rate Setting – Optimize Sample Rate: 通过最高频率分析优化之后的值,并不知道啥意思~
  • Sample Rate Setting – Override Sample Rate: 自定义的采样率的值。建议用默认的。

如果你觉得这篇文章对你有帮助,可以顺手点个,不但不会喜当爹,还能让更多人能看到它… Unity3d开发(十五) AudioClip 参数解析

分类: 未分类 标签:

[置顶] Cocos开发VR菜鸟宝典 第七讲:平台的选择与接入

2016年6月23日 没有评论

火云开发课堂开讲啦!

Cocos开发VR菜鸟宝典

        

                  本套课程分为基础班和高级班两种,基础班主要在CSDN上推出视频,学员自学为主。高级班在此基础上再进行项目实训(包含Cocos3D,Shader基础班和VR设备学习套装)

有兴趣参加高级班的可以加我QQ:285421210.

第七章:平台的选择与接入



视频地址:http://edu.csdn.net/course/detail/2366/36775?auto_start=1

[置顶]        Cocos开发VR菜鸟宝典 第七讲:平台的选择与接入

[置顶]        Cocos开发VR菜鸟宝典 第七讲:平台的选择与接入

[置顶]        Cocos开发VR菜鸟宝典 第七讲:平台的选择与接入

[置顶]        Cocos开发VR菜鸟宝典 第七讲:平台的选择与接入

[置顶]        Cocos开发VR菜鸟宝典 第七讲:平台的选择与接入

[置顶]        Cocos开发VR菜鸟宝典 第七讲:平台的选择与接入

[置顶]        Cocos开发VR菜鸟宝典 第七讲:平台的选择与接入

[置顶]        Cocos开发VR菜鸟宝典 第七讲:平台的选择与接入

[置顶]        Cocos开发VR菜鸟宝典 第七讲:平台的选择与接入

[置顶]        Cocos开发VR菜鸟宝典 第七讲:平台的选择与接入

[置顶]        Cocos开发VR菜鸟宝典 第七讲:平台的选择与接入


[置顶]        Cocos开发VR菜鸟宝典 第七讲:平台的选择与接入








分类: 未分类 标签: