├── docs ├── .gitignore ├── Gemfile ├── javascripts │ ├── main.js │ └── highlight.pack.js ├── images │ ├── body-bg.jpg │ ├── header-bg.jpg │ ├── sidebar-bg.jpg │ ├── highlight-bg.jpg │ ├── download-button.png │ └── github-button.png ├── assets │ ├── add-menu.gif │ ├── squeeler.png │ ├── sun-full.png │ ├── weatherbar.png │ ├── drag-object.png │ ├── new-project.png │ ├── preferences.png │ ├── target-info.png │ ├── weather-view.png │ ├── add-status-icon.png │ ├── create-outlet.png │ ├── image-view-size.png │ ├── new-cocoa-class.png │ ├── new-project-1.png │ ├── new-project-2.png │ ├── statusIcon-sun.png │ ├── weather-icons.zip │ ├── new_project_name.png │ ├── statusIcon-sun@2x.png │ ├── update-menu-item.png │ ├── weather-icons │ │ ├── 01d.png │ │ ├── 01n.png │ │ ├── 02d.png │ │ ├── 02n.png │ │ ├── 03d.png │ │ ├── 03n.png │ │ ├── 04d.png │ │ ├── 04n.png │ │ ├── 09d.png │ │ ├── 09n.png │ │ ├── 10d.png │ │ ├── 10n.png │ │ ├── 11d.png │ │ ├── 11n.png │ │ ├── 13d.png │ │ ├── 13n.png │ │ ├── 50d.png │ │ └── 50n.png │ ├── create-quit-action.png │ ├── weather-view-class.png │ ├── window-menu-delete.png │ ├── add-weather-menu-item.png │ ├── app-with-weather-view.png │ ├── reconnect-status-menu.png │ ├── weather-images-assets.png │ ├── weather-menu-item-tag.png │ ├── Info-plist-open-as-source.png │ ├── clear-delegate-status-menu.png │ └── set-statusmenucontroller-class.png ├── _config.yml ├── _includes │ └── image.html ├── _layouts │ └── default.html ├── Gemfile.lock ├── stylesheets │ ├── print.css │ └── stylesheet.css ├── params.json └── index.md ├── WeatherBar ├── Assets.xcassets │ ├── Contents.json │ ├── weather-icons │ │ ├── Contents.json │ │ ├── 01d.imageset │ │ │ ├── 01d.png │ │ │ └── Contents.json │ │ ├── 01n.imageset │ │ │ ├── 01n.png │ │ │ └── Contents.json │ │ ├── 02d.imageset │ │ │ ├── 02d.png │ │ │ └── Contents.json │ │ ├── 02n.imageset │ │ │ ├── 02n.png │ │ │ └── Contents.json │ │ ├── 03d.imageset │ │ │ ├── 03d.png │ │ │ └── Contents.json │ │ ├── 03n.imageset │ │ │ ├── 03n.png │ │ │ └── Contents.json │ │ ├── 04d.imageset │ │ │ ├── 04d.png │ │ │ └── Contents.json │ │ ├── 04n.imageset │ │ │ ├── 04n.png │ │ │ └── Contents.json │ │ ├── 09d.imageset │ │ │ ├── 09d.png │ │ │ └── Contents.json │ │ ├── 09n.imageset │ │ │ ├── 09n.png │ │ │ └── Contents.json │ │ ├── 10d.imageset │ │ │ ├── 10d.png │ │ │ └── Contents.json │ │ ├── 10n.imageset │ │ │ ├── 10n.png │ │ │ └── Contents.json │ │ ├── 11d.imageset │ │ │ ├── 11d.png │ │ │ └── Contents.json │ │ ├── 11n.imageset │ │ │ ├── 11n.png │ │ │ └── Contents.json │ │ ├── 13d.imageset │ │ │ ├── 13d.png │ │ │ └── Contents.json │ │ ├── 13n.imageset │ │ │ ├── 13n.png │ │ │ └── Contents.json │ │ ├── 50d.imageset │ │ │ ├── 50d.png │ │ │ └── Contents.json │ │ └── 50n.imageset │ │ │ ├── 50n.png │ │ │ └── Contents.json │ ├── statusIcon.imageset │ │ ├── statusIcon.png │ │ ├── statusIcon@2x.png │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── AppDelegate.swift ├── WeatherView.swift ├── PreferencesWindow.swift ├── Info.plist ├── StatusMenuController.swift ├── WeatherAPI.swift ├── PreferencesWindow.xib └── Base.lproj │ └── MainMenu.xib ├── README.md ├── WeatherBar.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── project.pbxproj ├── .gitignore ├── WeatherBarTests ├── Info.plist └── WeatherBarTests.swift ├── WeatherBarUITests ├── Info.plist └── WeatherBarUITests.swift └── LICENSE /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site/ 2 | .sass-cache/ 3 | .jekyll-metadata 4 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'github-pages' 3 | -------------------------------------------------------------------------------- /docs/javascripts/main.js: -------------------------------------------------------------------------------- 1 | console.log('This would be the main JS file.'); 2 | -------------------------------------------------------------------------------- /docs/images/body-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/images/body-bg.jpg -------------------------------------------------------------------------------- /docs/assets/add-menu.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/add-menu.gif -------------------------------------------------------------------------------- /docs/assets/squeeler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/squeeler.png -------------------------------------------------------------------------------- /docs/assets/sun-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/sun-full.png -------------------------------------------------------------------------------- /docs/assets/weatherbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weatherbar.png -------------------------------------------------------------------------------- /docs/images/header-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/images/header-bg.jpg -------------------------------------------------------------------------------- /docs/images/sidebar-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/images/sidebar-bg.jpg -------------------------------------------------------------------------------- /docs/assets/drag-object.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/drag-object.png -------------------------------------------------------------------------------- /docs/assets/new-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/new-project.png -------------------------------------------------------------------------------- /docs/assets/preferences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/preferences.png -------------------------------------------------------------------------------- /docs/assets/target-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/target-info.png -------------------------------------------------------------------------------- /docs/assets/weather-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-view.png -------------------------------------------------------------------------------- /docs/images/highlight-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/images/highlight-bg.jpg -------------------------------------------------------------------------------- /docs/assets/add-status-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/add-status-icon.png -------------------------------------------------------------------------------- /docs/assets/create-outlet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/create-outlet.png -------------------------------------------------------------------------------- /docs/assets/image-view-size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/image-view-size.png -------------------------------------------------------------------------------- /docs/assets/new-cocoa-class.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/new-cocoa-class.png -------------------------------------------------------------------------------- /docs/assets/new-project-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/new-project-1.png -------------------------------------------------------------------------------- /docs/assets/new-project-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/new-project-2.png -------------------------------------------------------------------------------- /docs/assets/statusIcon-sun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/statusIcon-sun.png -------------------------------------------------------------------------------- /docs/assets/weather-icons.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-icons.zip -------------------------------------------------------------------------------- /docs/images/download-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/images/download-button.png -------------------------------------------------------------------------------- /docs/images/github-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/images/github-button.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /docs/assets/new_project_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/new_project_name.png -------------------------------------------------------------------------------- /docs/assets/statusIcon-sun@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/statusIcon-sun@2x.png -------------------------------------------------------------------------------- /docs/assets/update-menu-item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/update-menu-item.png -------------------------------------------------------------------------------- /docs/assets/weather-icons/01d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-icons/01d.png -------------------------------------------------------------------------------- /docs/assets/weather-icons/01n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-icons/01n.png -------------------------------------------------------------------------------- /docs/assets/weather-icons/02d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-icons/02d.png -------------------------------------------------------------------------------- /docs/assets/weather-icons/02n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-icons/02n.png -------------------------------------------------------------------------------- /docs/assets/weather-icons/03d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-icons/03d.png -------------------------------------------------------------------------------- /docs/assets/weather-icons/03n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-icons/03n.png -------------------------------------------------------------------------------- /docs/assets/weather-icons/04d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-icons/04d.png -------------------------------------------------------------------------------- /docs/assets/weather-icons/04n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-icons/04n.png -------------------------------------------------------------------------------- /docs/assets/weather-icons/09d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-icons/09d.png -------------------------------------------------------------------------------- /docs/assets/weather-icons/09n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-icons/09n.png -------------------------------------------------------------------------------- /docs/assets/weather-icons/10d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-icons/10d.png -------------------------------------------------------------------------------- /docs/assets/weather-icons/10n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-icons/10n.png -------------------------------------------------------------------------------- /docs/assets/weather-icons/11d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-icons/11d.png -------------------------------------------------------------------------------- /docs/assets/weather-icons/11n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-icons/11n.png -------------------------------------------------------------------------------- /docs/assets/weather-icons/13d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-icons/13d.png -------------------------------------------------------------------------------- /docs/assets/weather-icons/13n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-icons/13n.png -------------------------------------------------------------------------------- /docs/assets/weather-icons/50d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-icons/50d.png -------------------------------------------------------------------------------- /docs/assets/weather-icons/50n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-icons/50n.png -------------------------------------------------------------------------------- /docs/assets/create-quit-action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/create-quit-action.png -------------------------------------------------------------------------------- /docs/assets/weather-view-class.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-view-class.png -------------------------------------------------------------------------------- /docs/assets/window-menu-delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/window-menu-delete.png -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | markdown: kramdown 2 | kramdown: 3 | input: GFM 4 | exclude: ["WeatherBar.xcodeproj"] 5 | highlighter: rouge 6 | -------------------------------------------------------------------------------- /docs/assets/add-weather-menu-item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/add-weather-menu-item.png -------------------------------------------------------------------------------- /docs/assets/app-with-weather-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/app-with-weather-view.png -------------------------------------------------------------------------------- /docs/assets/reconnect-status-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/reconnect-status-menu.png -------------------------------------------------------------------------------- /docs/assets/weather-images-assets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-images-assets.png -------------------------------------------------------------------------------- /docs/assets/weather-menu-item-tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/weather-menu-item-tag.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /docs/assets/Info-plist-open-as-source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/Info-plist-open-as-source.png -------------------------------------------------------------------------------- /docs/assets/clear-delegate-status-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/clear-delegate-status-menu.png -------------------------------------------------------------------------------- /docs/assets/set-statusmenucontroller-class.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/docs/assets/set-statusmenucontroller-class.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/statusIcon.imageset/statusIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/statusIcon.imageset/statusIcon.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/01d.imageset/01d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/weather-icons/01d.imageset/01d.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/01n.imageset/01n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/weather-icons/01n.imageset/01n.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/02d.imageset/02d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/weather-icons/02d.imageset/02d.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/02n.imageset/02n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/weather-icons/02n.imageset/02n.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/03d.imageset/03d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/weather-icons/03d.imageset/03d.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/03n.imageset/03n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/weather-icons/03n.imageset/03n.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/04d.imageset/04d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/weather-icons/04d.imageset/04d.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/04n.imageset/04n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/weather-icons/04n.imageset/04n.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/09d.imageset/09d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/weather-icons/09d.imageset/09d.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/09n.imageset/09n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/weather-icons/09n.imageset/09n.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/10d.imageset/10d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/weather-icons/10d.imageset/10d.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/10n.imageset/10n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/weather-icons/10n.imageset/10n.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/11d.imageset/11d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/weather-icons/11d.imageset/11d.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/11n.imageset/11n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/weather-icons/11n.imageset/11n.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/13d.imageset/13d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/weather-icons/13d.imageset/13d.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/13n.imageset/13n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/weather-icons/13n.imageset/13n.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/50d.imageset/50d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/weather-icons/50d.imageset/50d.png -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/50n.imageset/50n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/weather-icons/50n.imageset/50n.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WeatherBar 2 | ========== 3 | 4 | Demo weather app for Etsy School class on writing a Mac app in Swift 5 | 6 | See the tutorial at http://footle.org/WeatherBar/ 7 | 8 | -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/statusIcon.imageset/statusIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgreenlee/WeatherBar/HEAD/WeatherBar/Assets.xcassets/statusIcon.imageset/statusIcon@2x.png -------------------------------------------------------------------------------- /docs/_includes/image.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
{{ include.description }}
{{ include.description }}
-------------------------------------------------------------------------------- /WeatherBar.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/01d.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "01d.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/01n.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "01n.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/02d.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "02d.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/02n.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "02n.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/03d.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "03d.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/03n.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "03n.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/04d.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "04d.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/04n.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "04n.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/09d.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "09d.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/09n.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "09n.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/10d.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "10d.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/10n.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "10n.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/11d.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "11d.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/11n.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "11n.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/13d.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "13d.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/13n.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "13n.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/50d.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "50d.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/weather-icons/50n.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "50n.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/statusIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "statusIcon.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "statusIcon@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /WeatherBar/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // WeatherBar 4 | // 5 | // Created by Brad Greenlee on 10/10/15. 6 | // Copyright © 2015 Etsy. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | func applicationDidFinishLaunching(_ aNotification: Notification) { 14 | // Insert code here to initialize your application 15 | } 16 | 17 | func applicationWillTerminate(_ aNotification: Notification) { 18 | // Insert code here to tear down your application 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac 2 | .DS_Store 3 | 4 | # Xcode 5 | # 6 | build/ 7 | *.pbxuser 8 | !default.pbxuser 9 | *.mode1v3 10 | !default.mode1v3 11 | *.mode2v3 12 | !default.mode2v3 13 | *.perspectivev3 14 | !default.perspectivev3 15 | xcuserdata 16 | *.xccheckout 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | *.xcuserstate 22 | 23 | # CocoaPods 24 | # 25 | # We recommend against adding the Pods directory to your .gitignore. However 26 | # you should judge for yourself, the pros and cons are mentioned at: 27 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 28 | # 29 | # Pods/ 30 | 31 | # Carthage 32 | # 33 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 34 | # Carthage/Checkouts 35 | 36 | Carthage/Build 37 | -------------------------------------------------------------------------------- /WeatherBar/WeatherView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherView.swift 3 | // WeatherBar 4 | // 5 | // Created by Brad Greenlee on 10/13/15. 6 | // Copyright © 2015 Etsy. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class WeatherView: NSView { 12 | @IBOutlet weak var imageView: NSImageView! 13 | @IBOutlet weak var cityTextField: NSTextField! 14 | @IBOutlet weak var currentConditionsTextField: NSTextField! 15 | 16 | func update(_ weather: Weather) { 17 | // do UI updates on the main thread 18 | DispatchQueue.main.async { 19 | self.cityTextField.stringValue = weather.city 20 | self.currentConditionsTextField.stringValue = "\(Int(weather.currentTemp))°F and \(weather.conditions)" 21 | self.imageView.image = NSImage(named: weather.icon) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /WeatherBarTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /WeatherBarUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Brad Greenlee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /WeatherBarTests/WeatherBarTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherBarTests.swift 3 | // WeatherBarTests 4 | // 5 | // Created by Brad Greenlee on 10/10/15. 6 | // Copyright © 2015 Etsy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import WeatherBar 11 | 12 | class WeatherBarTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /WeatherBar/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /WeatherBar/PreferencesWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreferencesWindow.swift 3 | // WeatherBar 4 | // 5 | // Created by Brad Greenlee on 10/13/15. 6 | // Copyright © 2015 Etsy. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | protocol PreferencesWindowDelegate { 12 | func preferencesDidUpdate() 13 | } 14 | 15 | class PreferencesWindow: NSWindowController, NSWindowDelegate { 16 | var delegate: PreferencesWindowDelegate? 17 | @IBOutlet weak var cityTextField: NSTextField! 18 | 19 | override var windowNibName : String! { 20 | return "PreferencesWindow" 21 | } 22 | 23 | override func windowDidLoad() { 24 | super.windowDidLoad() 25 | 26 | self.window?.center() 27 | self.window?.makeKeyAndOrderFront(nil) 28 | NSApp.activate(ignoringOtherApps: true) 29 | 30 | let defaults = UserDefaults.standard 31 | let city = defaults.string(forKey: "city") ?? DEFAULT_CITY 32 | cityTextField.stringValue = city 33 | } 34 | 35 | func windowWillClose(_ notification: Notification) { 36 | let defaults = UserDefaults.standard 37 | defaults.setValue(cityTextField.stringValue, forKey: "city") 38 | delegate?.preferencesDidUpdate() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /WeatherBarUITests/WeatherBarUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherBarUITests.swift 3 | // WeatherBarUITests 4 | // 5 | // Created by Brad Greenlee on 10/10/15. 6 | // Copyright © 2015 Etsy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class WeatherBarUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | 18 | // In UI tests it is usually best to stop immediately when a failure occurs. 19 | continueAfterFailure = false 20 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 21 | XCUIApplication().launch() 22 | 23 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | super.tearDown() 29 | } 30 | 31 | func testExample() { 32 | // Use recording to get started writing UI tests. 33 | // Use XCTAssert and related functions to verify your tests produce the correct results. 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /WeatherBar/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | LSUIElement 28 | 29 | NSHumanReadableCopyright 30 | Copyright © 2015 Etsy. All rights reserved. 31 | NSMainNibFile 32 | MainMenu 33 | NSPrincipalClass 34 | NSApplication 35 | NSAppTransportSecurity 36 | 37 | NSExceptionDomains 38 | 39 | api.openweathermap.org 40 | 41 | NSExceptionAllowsInsecureHTTPLoads 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | WeatherBar by bgreenlee 17 | 18 | 19 | 20 |
21 |
22 |

WeatherBar

23 |

Write a Mac App in Swift!

24 | View project onGitHub 25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 | {{ content }} 33 | 34 |
35 | 36 | 42 |
43 |
44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /WeatherBar/StatusMenuController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusMenuController.swift 3 | // WeatherBar 4 | // 5 | // Created by Brad Greenlee on 10/11/15. 6 | // Copyright © 2015 Etsy. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | let DEFAULT_CITY = "Seattle, WA" 12 | 13 | class StatusMenuController: NSObject, PreferencesWindowDelegate { 14 | @IBOutlet weak var statusMenu: NSMenu! 15 | @IBOutlet weak var weatherView: WeatherView! 16 | 17 | var weatherMenuItem: NSMenuItem! 18 | var preferencesWindow: PreferencesWindow! 19 | 20 | let statusItem = NSStatusBar.system().statusItem(withLength: NSVariableStatusItemLength) 21 | let weatherAPI = WeatherAPI() 22 | 23 | override func awakeFromNib() { 24 | statusItem.menu = statusMenu 25 | let icon = NSImage(named: "statusIcon") 26 | icon?.isTemplate = true // best for dark mode 27 | statusItem.image = icon 28 | statusItem.menu = statusMenu 29 | weatherMenuItem = statusMenu.item(withTitle: "Weather") 30 | weatherMenuItem.view = weatherView 31 | preferencesWindow = PreferencesWindow() 32 | preferencesWindow.delegate = self 33 | 34 | updateWeather() 35 | } 36 | 37 | func updateWeather() { 38 | let defaults = UserDefaults.standard 39 | let city = defaults.string(forKey: "city") ?? DEFAULT_CITY 40 | weatherAPI.fetchWeather(city) { weather in 41 | self.weatherView.update(weather) 42 | } 43 | } 44 | 45 | @IBAction func updateClicked(_ sender: NSMenuItem) { 46 | updateWeather() 47 | } 48 | 49 | @IBAction func preferencesClicked(_ sender: NSMenuItem) { 50 | preferencesWindow.showWindow(nil) 51 | } 52 | 53 | @IBAction func quitClicked(_ sender: NSMenuItem) { 54 | NSApplication.shared().terminate(self) 55 | } 56 | 57 | func preferencesDidUpdate() { 58 | updateWeather() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /WeatherBar/WeatherAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherAPI.swift 3 | // WeatherBar 4 | // 5 | // Created by Brad Greenlee on 10/11/15. 6 | // Copyright © 2015 Etsy. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Weather: CustomStringConvertible { 12 | var city: String 13 | var currentTemp: Float 14 | var conditions: String 15 | var icon: String 16 | 17 | var description: String { 18 | return "\(city): \(currentTemp)F and \(conditions)" 19 | } 20 | } 21 | 22 | class WeatherAPI { 23 | let API_KEY = "your-api-key-here" 24 | let BASE_URL = "http://api.openweathermap.org/data/2.5/weather" 25 | 26 | func fetchWeather(_ query: String, success: @escaping (Weather) -> Void) { 27 | let session = URLSession.shared 28 | // url-escape the query string we're passed 29 | let escapedQuery = query.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) 30 | let url = URL(string: "\(BASE_URL)?APPID=\(API_KEY)&units=imperial&q=\(escapedQuery!)") 31 | let task = session.dataTask(with: url!) { data, response, err in 32 | // first check for a hard error 33 | if let error = err { 34 | NSLog("weather api error: \(error)") 35 | } 36 | 37 | // then check the response code 38 | if let httpResponse = response as? HTTPURLResponse { 39 | switch httpResponse.statusCode { 40 | case 200: // all good! 41 | if let weather = self.weatherFromJSONData(data!) { 42 | success(weather) 43 | } 44 | case 401: // unauthorized 45 | NSLog("weather api returned an 'unauthorized' response. Did you set your API key?") 46 | default: 47 | NSLog("weather api returned response: %d %@", httpResponse.statusCode, HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode)) 48 | } 49 | } 50 | } 51 | task.resume() 52 | } 53 | 54 | func weatherFromJSONData(_ data: Data) -> Weather? { 55 | typealias JSONDict = [String:AnyObject] 56 | let json : JSONDict 57 | 58 | do { 59 | json = try JSONSerialization.jsonObject(with: data, options: []) as! JSONDict 60 | } catch { 61 | NSLog("JSON parsing failed: \(error)") 62 | return nil 63 | } 64 | 65 | var mainDict = json["main"] as! JSONDict 66 | var weatherList = json["weather"] as! [JSONDict] 67 | var weatherDict = weatherList[0] 68 | 69 | let weather = Weather( 70 | city: json["name"] as! String, 71 | currentTemp: mainDict["temp"] as! Float, 72 | conditions: weatherDict["main"] as! String, 73 | icon: weatherDict["icon"] as! String 74 | ) 75 | 76 | return weather 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /WeatherBar/PreferencesWindow.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | RedCloth (4.2.9) 5 | activesupport (4.2.4) 6 | i18n (~> 0.7) 7 | json (~> 1.7, >= 1.7.7) 8 | minitest (~> 5.1) 9 | thread_safe (~> 0.3, >= 0.3.4) 10 | tzinfo (~> 1.1) 11 | addressable (2.3.8) 12 | blankslate (2.1.2.4) 13 | classifier-reborn (2.0.3) 14 | fast-stemmer (~> 1.0) 15 | coffee-script (2.4.1) 16 | coffee-script-source 17 | execjs 18 | coffee-script-source (1.9.1.1) 19 | colorator (0.1) 20 | ethon (0.8.0) 21 | ffi (>= 1.3.0) 22 | execjs (2.6.0) 23 | fast-stemmer (1.0.2) 24 | ffi (1.9.10) 25 | gemoji (2.1.0) 26 | github-pages (39) 27 | RedCloth (= 4.2.9) 28 | github-pages-health-check (~> 0.2) 29 | jekyll (= 2.4.0) 30 | jekyll-coffeescript (= 1.0.1) 31 | jekyll-feed (= 0.3.1) 32 | jekyll-mentions (= 0.2.1) 33 | jekyll-redirect-from (= 0.8.0) 34 | jekyll-sass-converter (= 1.3.0) 35 | jekyll-sitemap (= 0.8.1) 36 | jemoji (= 0.5.0) 37 | kramdown (= 1.5.0) 38 | liquid (= 2.6.2) 39 | maruku (= 0.7.0) 40 | mercenary (~> 0.3) 41 | pygments.rb (= 0.6.3) 42 | rdiscount (= 2.1.7) 43 | redcarpet (= 3.3.2) 44 | terminal-table (~> 1.4) 45 | github-pages-health-check (0.5.3) 46 | addressable (~> 2.3) 47 | net-dns (~> 0.8) 48 | public_suffix (~> 1.4) 49 | typhoeus (~> 0.7) 50 | html-pipeline (1.9.0) 51 | activesupport (>= 2) 52 | nokogiri (~> 1.4) 53 | i18n (0.7.0) 54 | jekyll (2.4.0) 55 | classifier-reborn (~> 2.0) 56 | colorator (~> 0.1) 57 | jekyll-coffeescript (~> 1.0) 58 | jekyll-gist (~> 1.0) 59 | jekyll-paginate (~> 1.0) 60 | jekyll-sass-converter (~> 1.0) 61 | jekyll-watch (~> 1.1) 62 | kramdown (~> 1.3) 63 | liquid (~> 2.6.1) 64 | mercenary (~> 0.3.3) 65 | pygments.rb (~> 0.6.0) 66 | redcarpet (~> 3.1) 67 | safe_yaml (~> 1.0) 68 | toml (~> 0.1.0) 69 | jekyll-coffeescript (1.0.1) 70 | coffee-script (~> 2.2) 71 | jekyll-feed (0.3.1) 72 | jekyll-gist (1.3.4) 73 | jekyll-mentions (0.2.1) 74 | html-pipeline (~> 1.9.0) 75 | jekyll (~> 2.0) 76 | jekyll-paginate (1.1.0) 77 | jekyll-redirect-from (0.8.0) 78 | jekyll (>= 2.0) 79 | jekyll-sass-converter (1.3.0) 80 | sass (~> 3.2) 81 | jekyll-sitemap (0.8.1) 82 | jekyll-watch (1.3.0) 83 | listen (~> 3.0) 84 | jemoji (0.5.0) 85 | gemoji (~> 2.0) 86 | html-pipeline (~> 1.9) 87 | jekyll (>= 2.0) 88 | json (1.8.3) 89 | kramdown (1.5.0) 90 | liquid (2.6.2) 91 | listen (3.0.3) 92 | rb-fsevent (>= 0.9.3) 93 | rb-inotify (>= 0.9) 94 | maruku (0.7.0) 95 | mercenary (0.3.5) 96 | mini_portile (0.6.2) 97 | minitest (5.8.1) 98 | net-dns (0.8.0) 99 | nokogiri (1.6.6.2) 100 | mini_portile (~> 0.6.0) 101 | parslet (1.5.0) 102 | blankslate (~> 2.0) 103 | posix-spawn (0.3.11) 104 | public_suffix (1.5.1) 105 | pygments.rb (0.6.3) 106 | posix-spawn (~> 0.3.6) 107 | yajl-ruby (~> 1.2.0) 108 | rb-fsevent (0.9.6) 109 | rb-inotify (0.9.5) 110 | ffi (>= 0.5.0) 111 | rdiscount (2.1.7) 112 | redcarpet (3.3.2) 113 | safe_yaml (1.0.4) 114 | sass (3.4.19) 115 | terminal-table (1.5.2) 116 | thread_safe (0.3.5) 117 | toml (0.1.2) 118 | parslet (~> 1.5.0) 119 | typhoeus (0.8.0) 120 | ethon (>= 0.8.0) 121 | tzinfo (1.2.2) 122 | thread_safe (~> 0.1) 123 | yajl-ruby (1.2.1) 124 | 125 | PLATFORMS 126 | ruby 127 | 128 | DEPENDENCIES 129 | github-pages 130 | -------------------------------------------------------------------------------- /docs/stylesheets/print.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, hgroup, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video { 14 | margin: 0; 15 | padding: 0; 16 | border: 0; 17 | font-size: 100%; 18 | font: inherit; 19 | vertical-align: baseline; 20 | } 21 | /* HTML5 display-role reset for older browsers */ 22 | article, aside, details, figcaption, figure, 23 | footer, header, hgroup, menu, nav, section { 24 | display: block; 25 | } 26 | body { 27 | line-height: 1; 28 | } 29 | ol, ul { 30 | list-style: none; 31 | } 32 | blockquote, q { 33 | quotes: none; 34 | } 35 | blockquote:before, blockquote:after, 36 | q:before, q:after { 37 | content: ''; 38 | content: none; 39 | } 40 | table { 41 | border-collapse: collapse; 42 | border-spacing: 0; 43 | } 44 | body { 45 | font-size: 13px; 46 | line-height: 1.5; 47 | font-family: 'Helvetica Neue', Helvetica, Arial, serif; 48 | color: #000; 49 | } 50 | 51 | a { 52 | color: #d5000d; 53 | font-weight: bold; 54 | } 55 | 56 | header { 57 | padding-top: 35px; 58 | padding-bottom: 10px; 59 | } 60 | 61 | header h1 { 62 | font-weight: bold; 63 | letter-spacing: -1px; 64 | font-size: 48px; 65 | color: #303030; 66 | line-height: 1.2; 67 | } 68 | 69 | header h2 { 70 | letter-spacing: -1px; 71 | font-size: 24px; 72 | color: #aaa; 73 | font-weight: normal; 74 | line-height: 1.3; 75 | } 76 | #downloads { 77 | display: none; 78 | } 79 | #main_content { 80 | padding-top: 20px; 81 | } 82 | 83 | code, pre { 84 | font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal; 85 | color: #222; 86 | margin-bottom: 30px; 87 | font-size: 12px; 88 | } 89 | 90 | code { 91 | padding: 0 3px; 92 | } 93 | 94 | pre { 95 | border: solid 1px #ddd; 96 | padding: 20px; 97 | overflow: auto; 98 | } 99 | pre code { 100 | padding: 0; 101 | } 102 | 103 | ul, ol, dl { 104 | margin-bottom: 20px; 105 | } 106 | 107 | 108 | /* COMMON STYLES */ 109 | 110 | table { 111 | width: 100%; 112 | border: 1px solid #ebebeb; 113 | } 114 | 115 | th { 116 | font-weight: 500; 117 | } 118 | 119 | td { 120 | border: 1px solid #ebebeb; 121 | text-align: center; 122 | font-weight: 300; 123 | } 124 | 125 | form { 126 | background: #f2f2f2; 127 | padding: 20px; 128 | 129 | } 130 | 131 | 132 | /* GENERAL ELEMENT TYPE STYLES */ 133 | 134 | h1 { 135 | font-size: 2.8em; 136 | } 137 | 138 | h2 { 139 | font-size: 22px; 140 | font-weight: bold; 141 | color: #303030; 142 | margin-bottom: 8px; 143 | } 144 | 145 | h3 { 146 | color: #d5000d; 147 | font-size: 18px; 148 | font-weight: bold; 149 | margin-bottom: 8px; 150 | } 151 | 152 | h4 { 153 | font-size: 16px; 154 | color: #303030; 155 | font-weight: bold; 156 | } 157 | 158 | h5 { 159 | font-size: 1em; 160 | color: #303030; 161 | } 162 | 163 | h6 { 164 | font-size: .8em; 165 | color: #303030; 166 | } 167 | 168 | p { 169 | font-weight: 300; 170 | margin-bottom: 20px; 171 | } 172 | 173 | a { 174 | text-decoration: none; 175 | } 176 | 177 | p a { 178 | font-weight: 400; 179 | } 180 | 181 | blockquote { 182 | font-size: 1.6em; 183 | border-left: 10px solid #e9e9e9; 184 | margin-bottom: 20px; 185 | padding: 0 0 0 30px; 186 | } 187 | 188 | ul li { 189 | list-style: disc inside; 190 | padding-left: 20px; 191 | } 192 | 193 | ol li { 194 | list-style: decimal inside; 195 | padding-left: 3px; 196 | } 197 | 198 | dl dd { 199 | font-style: italic; 200 | font-weight: 100; 201 | } 202 | 203 | footer { 204 | margin-top: 40px; 205 | padding-top: 20px; 206 | padding-bottom: 30px; 207 | font-size: 13px; 208 | color: #aaa; 209 | } 210 | 211 | footer a { 212 | color: #666; 213 | } 214 | 215 | /* MISC */ 216 | .clearfix:after { 217 | clear: both; 218 | content: '.'; 219 | display: block; 220 | visibility: hidden; 221 | height: 0; 222 | } 223 | 224 | .clearfix {display: inline-block;} 225 | * html .clearfix {height: 1%;} 226 | .clearfix {display: block;} -------------------------------------------------------------------------------- /WeatherBar/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /docs/javascripts/highlight.pack.js: -------------------------------------------------------------------------------- 1 | /*! highlight.js v9.9.0 | BSD3 License | git.io/hljslicense */ 2 | !function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/[&<>]/gm,function(e){return I[e]})}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function i(e){return k.test(e)}function a(e){var n,t,r,a,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return R(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(a=o[n],i(a)||R(a))return a}function o(e,n){var t,r={};for(t in e)r[t]=e[t];if(n)for(t in n)r[t]=n[t];return r}function u(e){var n=[];return function r(e,i){for(var a=e.firstChild;a;a=a.nextSibling)3===a.nodeType?i+=a.nodeValue.length:1===a.nodeType&&(n.push({event:"start",offset:i,node:a}),i=r(a,i),t(a).match(/br|hr|img|input/)||n.push({event:"stop",offset:i,node:a}));return i}(e,0),n}function c(e,r,i){function a(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function u(e){l+=""}function c(e){("start"===e.event?o:u)(e.node)}for(var s=0,l="",f=[];e.length||r.length;){var g=a();if(l+=n(i.substring(s,g[0].offset)),s=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=a();while(g===e&&g.length&&g[0].offset===s);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return l+n(i.substr(s))}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(i,a){if(!i.compiled){if(i.compiled=!0,i.k=i.k||i.bK,i.k){var u={},c=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");u[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof i.k?c("keyword",i.k):E(i.k).forEach(function(e){c(e,i.k[e])}),i.k=u}i.lR=t(i.l||/\w+/,!0),a&&(i.bK&&(i.b="\\b("+i.bK.split(" ").join("|")+")\\b"),i.b||(i.b=/\B|\b/),i.bR=t(i.b),i.e||i.eW||(i.e=/\B|\b/),i.e&&(i.eR=t(i.e)),i.tE=n(i.e)||"",i.eW&&a.tE&&(i.tE+=(i.e?"|":"")+a.tE)),i.i&&(i.iR=t(i.i)),null==i.r&&(i.r=1),i.c||(i.c=[]);var s=[];i.c.forEach(function(e){e.v?e.v.forEach(function(n){s.push(o(e,n))}):s.push("self"===e?i:e)}),i.c=s,i.c.forEach(function(e){r(e,i)}),i.starts&&r(i.starts,a);var l=i.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([i.tE,i.i]).map(n).filter(Boolean);i.t=l.length?t(l.join("|"),!0):{exec:function(){return null}}}}r(e)}function l(e,t,i,a){function o(e,n){var t,i;for(t=0,i=n.c.length;i>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!i&&r(n.iR,e)}function g(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function h(e,n,t,r){var i=r?"":y.classPrefix,a='',a+n+o}function p(){var e,t,r,i;if(!E.k)return n(B);for(i="",t=0,E.lR.lastIndex=0,r=E.lR.exec(B);r;)i+=n(B.substring(t,r.index)),e=g(E,r),e?(M+=e[1],i+=h(e[0],n(r[0]))):i+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(B);return i+n(B.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!x[E.sL])return n(B);var t=e?l(E.sL,B,!0,L[E.sL]):f(B,E.sL.length?E.sL:void 0);return E.r>0&&(M+=t.r),e&&(L[E.sL]=t.top),h(t.language,t.value,!1,!0)}function b(){k+=null!=E.sL?d():p(),B=""}function v(e){k+=e.cN?h(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(B+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?B+=n:(t.eB&&(B+=n),b(),t.rB||t.eB||(B=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var i=E;i.skip?B+=n:(i.rE||i.eE||(B+=n),b(),i.eE&&(B=n));do E.cN&&(k+=C),E.skip||(M+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),i.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return B+=n,n.length||1}var N=R(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var w,E=a||N,L={},k="";for(w=E;w!==N;w=w.parent)w.cN&&(k=h(w.cN,"",!0)+k);var B="",M=0;try{for(var I,j,O=0;;){if(E.t.lastIndex=O,I=E.t.exec(t),!I)break;j=m(t.substring(O,I.index),I[0]),O=I.index+j}for(m(t.substr(O)),w=E;w.parent;w=w.parent)w.cN&&(k+=C);return{r:M,value:k,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function f(e,t){t=t||y.languages||E(x);var r={r:0,value:n(e)},i=r;return t.filter(R).forEach(function(n){var t=l(n,e,!1);t.language=n,t.r>i.r&&(i=t),t.r>r.r&&(i=r,r=t)}),i.language&&(r.second_best=i),r}function g(e){return y.tabReplace||y.useBR?e.replace(M,function(e,n){return y.useBR&&"\n"===e?"
":y.tabReplace?n.replace(/\t/g,y.tabReplace):void 0}):e}function h(e,n,t){var r=n?L[n]:t,i=[e.trim()];return e.match(/\bhljs\b/)||i.push("hljs"),-1===e.indexOf(r)&&i.push(r),i.join(" ").trim()}function p(e){var n,t,r,o,s,p=a(e);i(p)||(y.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):n=e,s=n.textContent,r=p?l(p,s,!0):f(s),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),s)),r.value=g(r.value),e.innerHTML=r.value,e.className=h(e.className,p,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function d(e){y=o(y,e)}function b(){if(!b.called){b.called=!0;var e=document.querySelectorAll("pre code");w.forEach.call(e,p)}}function v(){addEventListener("DOMContentLoaded",b,!1),addEventListener("load",b,!1)}function m(n,t){var r=x[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function N(){return E(x)}function R(e){return e=(e||"").toLowerCase(),x[e]||x[L[e]]}var w=[],E=Object.keys,x={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C="
",y={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},I={"&":"&","<":"<",">":">"};return e.highlight=l,e.highlightAuto=f,e.fixMarkup=g,e.highlightBlock=p,e.configure=d,e.initHighlighting=b,e.initHighlightingOnLoad=v,e.registerLanguage=m,e.listLanguages=N,e.getLanguage=R,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(n,t,r){var i=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return i.c.push(e.PWM),i.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),i},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("swift",function(e){var t={keyword:"__COLUMN__ __FILE__ __FUNCTION__ __LINE__ as as! as? associativity break case catch class continue convenience default defer deinit didSet do dynamic dynamicType else enum extension fallthrough false final for func get guard if import in indirect infix init inout internal is lazy left let mutating nil none nonmutating operator optional override postfix precedence prefix private protocol Protocol public repeat required rethrows return right self Self set static struct subscript super switch throw throws true try try! try? Type typealias unowned var weak where while willSet",literal:"true false nil",built_in:"abs advance alignof alignofValue anyGenerator assert assertionFailure bridgeFromObjectiveC bridgeFromObjectiveCUnconditional bridgeToObjectiveC bridgeToObjectiveCUnconditional c contains count countElements countLeadingZeros debugPrint debugPrintln distance dropFirst dropLast dump encodeBitsAsWords enumerate equal fatalError filter find getBridgedObjectiveCType getVaList indices insertionSort isBridgedToObjectiveC isBridgedVerbatimToObjectiveC isUniquelyReferenced isUniquelyReferencedNonObjC join lazy lexicographicalCompare map max maxElement min minElement numericCast overlaps partition posix precondition preconditionFailure print println quickSort readLine reduce reflect reinterpretCast reverse roundUpToAlignment sizeof sizeofValue sort split startsWith stride strideof strideofValue swap toString transcode underestimateCount unsafeAddressOf unsafeBitCast unsafeDowncast unsafeUnwrap unsafeReflect withExtendedLifetime withObjectAtPlusZero withUnsafePointer withUnsafePointerToObject withUnsafeMutablePointer withUnsafeMutablePointers withUnsafePointer withUnsafePointers withVaList zip"},i={cN:"type",b:"\\b[A-Z][\\wÀ-ʸ']*",r:0},n=e.C("/\\*","\\*/",{c:["self"]}),r={cN:"subst",b:/\\\(/,e:"\\)",k:t,c:[]},a={cN:"number",b:"\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b",r:0},o=e.inherit(e.QSM,{c:[r,e.BE]});return r.c=[a],{k:t,c:[o,e.CLCM,n,i,a,{cN:"function",bK:"func",e:"{",eE:!0,c:[e.inherit(e.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{b://},{cN:"params",b:/\(/,e:/\)/,endsParent:!0,k:t,c:["self",a,o,e.CBCM,{b:":"}],i:/["']/}],i:/\[|%/},{cN:"class",bK:"struct protocol class extension enum",k:t,e:"\\{",eE:!0,c:[e.inherit(e.TM,{b:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/})]},{cN:"meta",b:"(@warn_unused_result|@exported|@lazy|@noescape|@NSCopying|@NSManaged|@objc|@convention|@required|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix|@autoclosure|@testable|@available|@nonobjc|@NSApplicationMain|@UIApplicationMain)"},{bK:"import",e:/$/,c:[e.CLCM,n]}]}});hljs.registerLanguage("json",function(e){var i={literal:"true false null"},n=[e.QSM,e.CNM],r={e:",",eW:!0,eE:!0,c:n,k:i},t={b:"{",e:"}",c:[{cN:"attr",b:/"/,e:/"/,c:[e.BE],i:"\\n"},e.inherit(r,{b:/:/})],i:"\\S"},c={b:"\\[",e:"\\]",c:[e.inherit(r)],i:"\\S"};return n.splice(n.length,0,t,c),{c:n,k:i,i:"\\S"}}); -------------------------------------------------------------------------------- /docs/stylesheets/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | html, body, div, span, applet, object, iframe, 6 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 7 | a, abbr, acronym, address, big, cite, code, 8 | del, dfn, em, img, ins, kbd, q, s, samp, 9 | small, strike, strong, sub, sup, tt, var, 10 | b, u, i, center, 11 | dl, dt, dd, ol, ul, li, 12 | fieldset, form, label, legend, 13 | table, caption, tbody, tfoot, thead, tr, th, td, 14 | article, aside, canvas, details, embed, 15 | figure, figcaption, footer, header, hgroup, 16 | menu, nav, output, ruby, section, summary, 17 | time, mark, audio, video { 18 | margin: 0; 19 | padding: 0; 20 | border: 0; 21 | font-size: 100%; 22 | font: inherit; 23 | vertical-align: baseline; 24 | } 25 | /* HTML5 display-role reset for older browsers */ 26 | article, aside, details, figcaption, figure, 27 | footer, header, hgroup, menu, nav, section { 28 | display: block; 29 | } 30 | body { 31 | line-height: 1; 32 | } 33 | ol, ul { 34 | list-style: none; 35 | } 36 | blockquote, q { 37 | quotes: none; 38 | } 39 | blockquote:before, blockquote:after, 40 | q:before, q:after { 41 | content: ''; 42 | content: none; 43 | } 44 | table { 45 | border-collapse: collapse; 46 | border-spacing: 0; 47 | } 48 | 49 | /* LAYOUT STYLES */ 50 | body { 51 | font-size: 18px; 52 | line-height: 1.5; 53 | background: #fafafa url(../images/body-bg.jpg) 0 0 repeat; 54 | font-family: 'Helvetica Neue', Helvetica, Arial, serif; 55 | font-weight: 400; 56 | color: #666; 57 | } 58 | 59 | a { 60 | color: #2879d0; 61 | } 62 | a:hover { 63 | color: #2268b2; 64 | } 65 | 66 | header { 67 | padding-top: 40px; 68 | padding-bottom: 40px; 69 | font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; 70 | background: #2e7bcf url(../images/header-bg.jpg) 0 0 repeat-x; 71 | border-bottom: solid 1px #275da1; 72 | } 73 | 74 | header h1 { 75 | letter-spacing: -1px; 76 | font-size: 72px; 77 | color: #fff; 78 | line-height: 1; 79 | margin-bottom: 0.2em; 80 | width: 540px; 81 | } 82 | 83 | header h2 { 84 | font-size: 26px; 85 | color: #9ddcff; 86 | font-weight: normal; 87 | line-height: 1.3; 88 | width: 540px; 89 | letter-spacing: 0; 90 | } 91 | 92 | .inner { 93 | position: relative; 94 | width: 1000px; 95 | margin: 0 auto; 96 | } 97 | 98 | #content-wrapper { 99 | border-top: solid 1px #fff; 100 | padding-top: 30px; 101 | } 102 | 103 | #main-content { 104 | width: 780px; 105 | float: left; 106 | } 107 | 108 | #main-content img { 109 | max-width: 100%; 110 | } 111 | 112 | aside#sidebar { 113 | width: 200px; 114 | padding-left: 20px; 115 | min-height: 504px; 116 | float: right; 117 | background: transparent url(../images/sidebar-bg.jpg) 0 0 no-repeat; 118 | font-size: 12px; 119 | line-height: 1.3; 120 | } 121 | 122 | aside#sidebar p.repo-owner, 123 | aside#sidebar p.repo-owner a { 124 | font-weight: bold; 125 | } 126 | 127 | #downloads { 128 | margin-bottom: 40px; 129 | } 130 | 131 | a.button { 132 | width: 134px; 133 | height: 58px; 134 | line-height: 1.2; 135 | font-size: 23px; 136 | color: #fff; 137 | padding-left: 68px; 138 | padding-top: 22px; 139 | font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; 140 | } 141 | a.button small { 142 | display: block; 143 | font-size: 11px; 144 | } 145 | header a.button { 146 | position: absolute; 147 | right: 0; 148 | top: 0; 149 | background: transparent url(../images/github-button.png) 0 0 no-repeat; 150 | } 151 | aside a.button { 152 | width: 138px; 153 | padding-left: 64px; 154 | display: block; 155 | background: transparent url(../images/download-button.png) 0 0 no-repeat; 156 | margin-bottom: 20px; 157 | font-size: 21px; 158 | } 159 | 160 | code, pre { 161 | font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace; 162 | color: #222; 163 | margin-bottom: 30px; 164 | font-size: 13px; 165 | } 166 | 167 | pre { 168 | text-shadow: none; 169 | overflow: auto; 170 | border: solid 1px #ccc; 171 | } 172 | 173 | ul, ol, dl { 174 | margin-bottom: 20px; 175 | } 176 | 177 | 178 | /* COMMON STYLES */ 179 | 180 | hr { 181 | height: 1px; 182 | line-height: 1px; 183 | margin-top: 1em; 184 | padding-bottom: 1em; 185 | border: none; 186 | background: transparent url('../images/hr.png') 0 0 no-repeat; 187 | } 188 | 189 | table { 190 | width: 100%; 191 | border: 1px solid #ebebeb; 192 | } 193 | 194 | th { 195 | font-weight: 500; 196 | } 197 | 198 | td { 199 | border: 1px solid #ebebeb; 200 | text-align: center; 201 | font-weight: 300; 202 | } 203 | 204 | form { 205 | background: #f2f2f2; 206 | padding: 20px; 207 | 208 | } 209 | 210 | 211 | /* GENERAL ELEMENT TYPE STYLES */ 212 | 213 | #main-content h1 { 214 | font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; 215 | font-size: 2.8em; 216 | letter-spacing: -1px; 217 | color: #474747; 218 | } 219 | 220 | #main-content h1:before { 221 | content: "/"; 222 | color: #9ddcff; 223 | padding-right: 0.3em; 224 | margin-left: -0.9em; 225 | } 226 | 227 | #main-content h2 { 228 | font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; 229 | font-size: 22px; 230 | font-weight: bold; 231 | margin-bottom: 8px; 232 | color: #474747; 233 | } 234 | #main-content h2:before { 235 | content: "//"; 236 | color: #9ddcff; 237 | padding-right: 0.3em; 238 | margin-left: -1.5em; 239 | } 240 | 241 | #main-content h3 { 242 | font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; 243 | font-size: 18px; 244 | font-weight: bold; 245 | margin-top: 24px; 246 | margin-bottom: 8px; 247 | color: #474747; 248 | } 249 | 250 | #main-content h3:before { 251 | content: "///"; 252 | color: #9ddcff; 253 | padding-right: 0.3em; 254 | margin-left: -2em; 255 | } 256 | 257 | #main-content h4 { 258 | font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; 259 | font-size: 15px; 260 | font-weight: bold; 261 | color: #474747; 262 | } 263 | 264 | h4:before { 265 | content: "////"; 266 | color: #9ddcff; 267 | padding-right: 0.3em; 268 | margin-left: -2.8em; 269 | } 270 | 271 | #main-content h5 { 272 | font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; 273 | font-size: 14px; 274 | color: #474747; 275 | } 276 | h5:before { 277 | content: "/////"; 278 | color: #9ddcff; 279 | padding-right: 0.3em; 280 | margin-left: -3.2em; 281 | } 282 | 283 | #main-content h6 { 284 | font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; 285 | font-size: .8em; 286 | color: #474747; 287 | } 288 | h6:before { 289 | content: "//////"; 290 | color: #9ddcff; 291 | padding-right: 0.3em; 292 | margin-left: -3.7em; 293 | } 294 | 295 | p { 296 | margin-bottom: 20px; 297 | } 298 | 299 | a { 300 | text-decoration: none; 301 | } 302 | 303 | p a { 304 | font-weight: 400; 305 | } 306 | 307 | blockquote { 308 | font-size: 1.6em; 309 | border-left: 10px solid #e9e9e9; 310 | margin-bottom: 20px; 311 | padding: 0 0 0 30px; 312 | } 313 | 314 | ul { 315 | list-style: disc inside; 316 | padding-left: 20px; 317 | } 318 | 319 | ol { 320 | list-style: decimal inside; 321 | padding-left: 3px; 322 | } 323 | 324 | dl dd { 325 | font-style: italic; 326 | font-weight: 100; 327 | } 328 | 329 | footer { 330 | background: transparent url('../images/hr.png') 0 0 no-repeat; 331 | margin-top: 40px; 332 | padding-top: 20px; 333 | padding-bottom: 30px; 334 | font-size: 13px; 335 | color: #aaa; 336 | } 337 | 338 | footer a { 339 | color: #666; 340 | } 341 | footer a:hover { 342 | color: #444; 343 | } 344 | 345 | /* MISC */ 346 | .clearfix:after { 347 | clear: both; 348 | content: '.'; 349 | display: block; 350 | visibility: hidden; 351 | height: 0; 352 | } 353 | 354 | .clearfix {display: inline-block;} 355 | * html .clearfix {height: 1%;} 356 | .clearfix {display: block;} 357 | 358 | /* #Media Queries 359 | ================================================== */ 360 | 361 | /* Smaller than standard 960 (devices and browsers) */ 362 | @media only screen and (max-width: 959px) {} 363 | 364 | /* Tablet Portrait size to standard 960 (devices and browsers) */ 365 | @media only screen and (min-width: 768px) and (max-width: 959px) { 366 | .inner { 367 | width: 740px; 368 | } 369 | header h1, header h2 { 370 | width: 340px; 371 | } 372 | header h1 { 373 | font-size: 60px; 374 | } 375 | header h2 { 376 | font-size: 30px; 377 | } 378 | #main-content { 379 | width: 490px; 380 | } 381 | #main-content h1:before, 382 | #main-content h2:before, 383 | #main-content h3:before, 384 | #main-content h4:before, 385 | #main-content h5:before, 386 | #main-content h6:before { 387 | content: none; 388 | padding-right: 0; 389 | margin-left: 0; 390 | } 391 | } 392 | 393 | /* All Mobile Sizes (devices and browser) */ 394 | @media only screen and (max-width: 767px) { 395 | .inner { 396 | width: 93%; 397 | } 398 | header { 399 | padding: 20px 0; 400 | } 401 | header .inner { 402 | position: relative; 403 | } 404 | header h1, header h2 { 405 | width: 100%; 406 | } 407 | header h1 { 408 | font-size: 48px; 409 | } 410 | header h2 { 411 | font-size: 24px; 412 | } 413 | header a.button { 414 | background-image: none; 415 | width: auto; 416 | height: auto; 417 | display: inline-block; 418 | margin-top: 15px; 419 | padding: 5px 10px; 420 | position: relative; 421 | text-align: center; 422 | font-size: 13px; 423 | line-height: 1; 424 | background-color: #9ddcff; 425 | color: #2879d0; 426 | -moz-border-radius: 5px; 427 | -webkit-border-radius: 5px; 428 | border-radius: 5px; 429 | } 430 | header a.button small { 431 | font-size: 13px; 432 | display: inline; 433 | } 434 | #main-content, 435 | aside#sidebar { 436 | float: none; 437 | width: 100% ! important; 438 | } 439 | aside#sidebar { 440 | background-image: none; 441 | margin-top: 20px; 442 | border-top: solid 1px #ddd; 443 | padding: 20px 0; 444 | min-height: 0; 445 | } 446 | aside#sidebar a.button { 447 | display: none; 448 | } 449 | #main-content h1:before, 450 | #main-content h2:before, 451 | #main-content h3:before, 452 | #main-content h4:before, 453 | #main-content h5:before, 454 | #main-content h6:before { 455 | content: none; 456 | padding-right: 0; 457 | margin-left: 0; 458 | } 459 | } 460 | 461 | /* Mobile Landscape Size to Tablet Portrait (devices and browsers) */ 462 | @media only screen and (min-width: 480px) and (max-width: 767px) {} 463 | 464 | /* Mobile Portrait Size to Mobile Landscape Size (devices and browsers) */ 465 | @media only screen and (max-width: 479px) {} 466 | 467 | /* customizations */ 468 | 469 | caption { 470 | text-align: left; 471 | font-size: 80%; 472 | margin-bottom: 1em; 473 | } 474 | 475 | table.image { 476 | width: initial; 477 | border: none; 478 | } 479 | 480 | table.image td { 481 | text-align: left; 482 | border: none; 483 | } 484 | 485 | strong { 486 | font-weight: bold; 487 | } 488 | 489 | pre { 490 | padding: 10px; 491 | } 492 | 493 | code, pre { 494 | margin-bottom: 0; 495 | } 496 | 497 | p { 498 | margin-top: 8px; 499 | margin-bottom: 16px; 500 | } 501 | 502 | #main-content h2 { 503 | margin-top: 8px; 504 | } 505 | 506 | ul { 507 | list-style: none; 508 | margin-left: 0; 509 | padding-left: 1em; 510 | text-indent: -1em; 511 | } 512 | 513 | ul li:before { 514 | content: "\0BB \020"; 515 | color: #9ddcff; 516 | } 517 | -------------------------------------------------------------------------------- /WeatherBar.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A21862A21BCDEA2E00770E87 /* PreferencesWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A21862A01BCDEA2E00770E87 /* PreferencesWindow.swift */; }; 11 | A21862A31BCDEA2E00770E87 /* PreferencesWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = A21862A11BCDEA2E00770E87 /* PreferencesWindow.xib */; }; 12 | A2B4247D1BCDE3D300887CB2 /* WeatherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2B4247C1BCDE3D300887CB2 /* WeatherView.swift */; }; 13 | A2D582281BCAAF37006A464B /* StatusMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2D582271BCAAF37006A464B /* StatusMenuController.swift */; }; 14 | A2D5822A1BCB6068006A464B /* WeatherAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2D582291BCB6068006A464B /* WeatherAPI.swift */; }; 15 | A2DEB14E1BC9980E004AAEB3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2DEB14D1BC9980E004AAEB3 /* AppDelegate.swift */; }; 16 | A2DEB1501BC9980E004AAEB3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A2DEB14F1BC9980E004AAEB3 /* Assets.xcassets */; }; 17 | A2DEB1531BC9980E004AAEB3 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = A2DEB1511BC9980E004AAEB3 /* MainMenu.xib */; }; 18 | A2DEB15E1BC9980E004AAEB3 /* WeatherBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2DEB15D1BC9980E004AAEB3 /* WeatherBarTests.swift */; }; 19 | A2DEB1691BC9980E004AAEB3 /* WeatherBarUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2DEB1681BC9980E004AAEB3 /* WeatherBarUITests.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXContainerItemProxy section */ 23 | A2DEB15A1BC9980E004AAEB3 /* PBXContainerItemProxy */ = { 24 | isa = PBXContainerItemProxy; 25 | containerPortal = A2DEB1421BC9980E004AAEB3 /* Project object */; 26 | proxyType = 1; 27 | remoteGlobalIDString = A2DEB1491BC9980E004AAEB3; 28 | remoteInfo = WeatherBar; 29 | }; 30 | A2DEB1651BC9980E004AAEB3 /* PBXContainerItemProxy */ = { 31 | isa = PBXContainerItemProxy; 32 | containerPortal = A2DEB1421BC9980E004AAEB3 /* Project object */; 33 | proxyType = 1; 34 | remoteGlobalIDString = A2DEB1491BC9980E004AAEB3; 35 | remoteInfo = WeatherBar; 36 | }; 37 | /* End PBXContainerItemProxy section */ 38 | 39 | /* Begin PBXFileReference section */ 40 | A21862A01BCDEA2E00770E87 /* PreferencesWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesWindow.swift; sourceTree = ""; }; 41 | A21862A11BCDEA2E00770E87 /* PreferencesWindow.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PreferencesWindow.xib; sourceTree = ""; }; 42 | A2B4247C1BCDE3D300887CB2 /* WeatherView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeatherView.swift; sourceTree = ""; }; 43 | A2D582271BCAAF37006A464B /* StatusMenuController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusMenuController.swift; sourceTree = ""; }; 44 | A2D582291BCB6068006A464B /* WeatherAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeatherAPI.swift; sourceTree = ""; }; 45 | A2DEB14A1BC9980E004AAEB3 /* WeatherBar.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WeatherBar.app; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | A2DEB14D1BC9980E004AAEB3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 47 | A2DEB14F1BC9980E004AAEB3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 48 | A2DEB1521BC9980E004AAEB3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 49 | A2DEB1541BC9980E004AAEB3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50 | A2DEB1591BC9980E004AAEB3 /* WeatherBarTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WeatherBarTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | A2DEB15D1BC9980E004AAEB3 /* WeatherBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherBarTests.swift; sourceTree = ""; }; 52 | A2DEB15F1BC9980E004AAEB3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53 | A2DEB1641BC9980E004AAEB3 /* WeatherBarUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WeatherBarUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | A2DEB1681BC9980E004AAEB3 /* WeatherBarUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherBarUITests.swift; sourceTree = ""; }; 55 | A2DEB16A1BC9980E004AAEB3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56 | /* End PBXFileReference section */ 57 | 58 | /* Begin PBXFrameworksBuildPhase section */ 59 | A2DEB1471BC9980E004AAEB3 /* Frameworks */ = { 60 | isa = PBXFrameworksBuildPhase; 61 | buildActionMask = 2147483647; 62 | files = ( 63 | ); 64 | runOnlyForDeploymentPostprocessing = 0; 65 | }; 66 | A2DEB1561BC9980E004AAEB3 /* Frameworks */ = { 67 | isa = PBXFrameworksBuildPhase; 68 | buildActionMask = 2147483647; 69 | files = ( 70 | ); 71 | runOnlyForDeploymentPostprocessing = 0; 72 | }; 73 | A2DEB1611BC9980E004AAEB3 /* Frameworks */ = { 74 | isa = PBXFrameworksBuildPhase; 75 | buildActionMask = 2147483647; 76 | files = ( 77 | ); 78 | runOnlyForDeploymentPostprocessing = 0; 79 | }; 80 | /* End PBXFrameworksBuildPhase section */ 81 | 82 | /* Begin PBXGroup section */ 83 | A2DEB1411BC9980E004AAEB3 = { 84 | isa = PBXGroup; 85 | children = ( 86 | A2DEB14C1BC9980E004AAEB3 /* WeatherBar */, 87 | A2DEB15C1BC9980E004AAEB3 /* WeatherBarTests */, 88 | A2DEB1671BC9980E004AAEB3 /* WeatherBarUITests */, 89 | A2DEB14B1BC9980E004AAEB3 /* Products */, 90 | ); 91 | sourceTree = ""; 92 | }; 93 | A2DEB14B1BC9980E004AAEB3 /* Products */ = { 94 | isa = PBXGroup; 95 | children = ( 96 | A2DEB14A1BC9980E004AAEB3 /* WeatherBar.app */, 97 | A2DEB1591BC9980E004AAEB3 /* WeatherBarTests.xctest */, 98 | A2DEB1641BC9980E004AAEB3 /* WeatherBarUITests.xctest */, 99 | ); 100 | name = Products; 101 | sourceTree = ""; 102 | }; 103 | A2DEB14C1BC9980E004AAEB3 /* WeatherBar */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | A2DEB14D1BC9980E004AAEB3 /* AppDelegate.swift */, 107 | A2D582271BCAAF37006A464B /* StatusMenuController.swift */, 108 | A2D582291BCB6068006A464B /* WeatherAPI.swift */, 109 | A2B4247C1BCDE3D300887CB2 /* WeatherView.swift */, 110 | A21862A01BCDEA2E00770E87 /* PreferencesWindow.swift */, 111 | A2DEB1511BC9980E004AAEB3 /* MainMenu.xib */, 112 | A21862A11BCDEA2E00770E87 /* PreferencesWindow.xib */, 113 | A2DEB14F1BC9980E004AAEB3 /* Assets.xcassets */, 114 | A2DEB1541BC9980E004AAEB3 /* Info.plist */, 115 | ); 116 | path = WeatherBar; 117 | sourceTree = ""; 118 | }; 119 | A2DEB15C1BC9980E004AAEB3 /* WeatherBarTests */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | A2DEB15D1BC9980E004AAEB3 /* WeatherBarTests.swift */, 123 | A2DEB15F1BC9980E004AAEB3 /* Info.plist */, 124 | ); 125 | path = WeatherBarTests; 126 | sourceTree = ""; 127 | }; 128 | A2DEB1671BC9980E004AAEB3 /* WeatherBarUITests */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | A2DEB1681BC9980E004AAEB3 /* WeatherBarUITests.swift */, 132 | A2DEB16A1BC9980E004AAEB3 /* Info.plist */, 133 | ); 134 | path = WeatherBarUITests; 135 | sourceTree = ""; 136 | }; 137 | /* End PBXGroup section */ 138 | 139 | /* Begin PBXNativeTarget section */ 140 | A2DEB1491BC9980E004AAEB3 /* WeatherBar */ = { 141 | isa = PBXNativeTarget; 142 | buildConfigurationList = A2DEB16D1BC9980E004AAEB3 /* Build configuration list for PBXNativeTarget "WeatherBar" */; 143 | buildPhases = ( 144 | A2DEB1461BC9980E004AAEB3 /* Sources */, 145 | A2DEB1471BC9980E004AAEB3 /* Frameworks */, 146 | A2DEB1481BC9980E004AAEB3 /* Resources */, 147 | ); 148 | buildRules = ( 149 | ); 150 | dependencies = ( 151 | ); 152 | name = WeatherBar; 153 | productName = WeatherBar; 154 | productReference = A2DEB14A1BC9980E004AAEB3 /* WeatherBar.app */; 155 | productType = "com.apple.product-type.application"; 156 | }; 157 | A2DEB1581BC9980E004AAEB3 /* WeatherBarTests */ = { 158 | isa = PBXNativeTarget; 159 | buildConfigurationList = A2DEB1701BC9980E004AAEB3 /* Build configuration list for PBXNativeTarget "WeatherBarTests" */; 160 | buildPhases = ( 161 | A2DEB1551BC9980E004AAEB3 /* Sources */, 162 | A2DEB1561BC9980E004AAEB3 /* Frameworks */, 163 | A2DEB1571BC9980E004AAEB3 /* Resources */, 164 | ); 165 | buildRules = ( 166 | ); 167 | dependencies = ( 168 | A2DEB15B1BC9980E004AAEB3 /* PBXTargetDependency */, 169 | ); 170 | name = WeatherBarTests; 171 | productName = WeatherBarTests; 172 | productReference = A2DEB1591BC9980E004AAEB3 /* WeatherBarTests.xctest */; 173 | productType = "com.apple.product-type.bundle.unit-test"; 174 | }; 175 | A2DEB1631BC9980E004AAEB3 /* WeatherBarUITests */ = { 176 | isa = PBXNativeTarget; 177 | buildConfigurationList = A2DEB1731BC9980E004AAEB3 /* Build configuration list for PBXNativeTarget "WeatherBarUITests" */; 178 | buildPhases = ( 179 | A2DEB1601BC9980E004AAEB3 /* Sources */, 180 | A2DEB1611BC9980E004AAEB3 /* Frameworks */, 181 | A2DEB1621BC9980E004AAEB3 /* Resources */, 182 | ); 183 | buildRules = ( 184 | ); 185 | dependencies = ( 186 | A2DEB1661BC9980E004AAEB3 /* PBXTargetDependency */, 187 | ); 188 | name = WeatherBarUITests; 189 | productName = WeatherBarUITests; 190 | productReference = A2DEB1641BC9980E004AAEB3 /* WeatherBarUITests.xctest */; 191 | productType = "com.apple.product-type.bundle.ui-testing"; 192 | }; 193 | /* End PBXNativeTarget section */ 194 | 195 | /* Begin PBXProject section */ 196 | A2DEB1421BC9980E004AAEB3 /* Project object */ = { 197 | isa = PBXProject; 198 | attributes = { 199 | LastUpgradeCheck = 0820; 200 | ORGANIZATIONNAME = Etsy; 201 | TargetAttributes = { 202 | A2DEB1491BC9980E004AAEB3 = { 203 | CreatedOnToolsVersion = 7.0.1; 204 | LastSwiftMigration = 0820; 205 | }; 206 | A2DEB1581BC9980E004AAEB3 = { 207 | CreatedOnToolsVersion = 7.0.1; 208 | LastSwiftMigration = 0820; 209 | TestTargetID = A2DEB1491BC9980E004AAEB3; 210 | }; 211 | A2DEB1631BC9980E004AAEB3 = { 212 | CreatedOnToolsVersion = 7.0.1; 213 | LastSwiftMigration = 0820; 214 | TestTargetID = A2DEB1491BC9980E004AAEB3; 215 | }; 216 | }; 217 | }; 218 | buildConfigurationList = A2DEB1451BC9980E004AAEB3 /* Build configuration list for PBXProject "WeatherBar" */; 219 | compatibilityVersion = "Xcode 3.2"; 220 | developmentRegion = English; 221 | hasScannedForEncodings = 0; 222 | knownRegions = ( 223 | en, 224 | Base, 225 | ); 226 | mainGroup = A2DEB1411BC9980E004AAEB3; 227 | productRefGroup = A2DEB14B1BC9980E004AAEB3 /* Products */; 228 | projectDirPath = ""; 229 | projectRoot = ""; 230 | targets = ( 231 | A2DEB1491BC9980E004AAEB3 /* WeatherBar */, 232 | A2DEB1581BC9980E004AAEB3 /* WeatherBarTests */, 233 | A2DEB1631BC9980E004AAEB3 /* WeatherBarUITests */, 234 | ); 235 | }; 236 | /* End PBXProject section */ 237 | 238 | /* Begin PBXResourcesBuildPhase section */ 239 | A2DEB1481BC9980E004AAEB3 /* Resources */ = { 240 | isa = PBXResourcesBuildPhase; 241 | buildActionMask = 2147483647; 242 | files = ( 243 | A2DEB1501BC9980E004AAEB3 /* Assets.xcassets in Resources */, 244 | A21862A31BCDEA2E00770E87 /* PreferencesWindow.xib in Resources */, 245 | A2DEB1531BC9980E004AAEB3 /* MainMenu.xib in Resources */, 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | }; 249 | A2DEB1571BC9980E004AAEB3 /* Resources */ = { 250 | isa = PBXResourcesBuildPhase; 251 | buildActionMask = 2147483647; 252 | files = ( 253 | ); 254 | runOnlyForDeploymentPostprocessing = 0; 255 | }; 256 | A2DEB1621BC9980E004AAEB3 /* Resources */ = { 257 | isa = PBXResourcesBuildPhase; 258 | buildActionMask = 2147483647; 259 | files = ( 260 | ); 261 | runOnlyForDeploymentPostprocessing = 0; 262 | }; 263 | /* End PBXResourcesBuildPhase section */ 264 | 265 | /* Begin PBXSourcesBuildPhase section */ 266 | A2DEB1461BC9980E004AAEB3 /* Sources */ = { 267 | isa = PBXSourcesBuildPhase; 268 | buildActionMask = 2147483647; 269 | files = ( 270 | A2D5822A1BCB6068006A464B /* WeatherAPI.swift in Sources */, 271 | A21862A21BCDEA2E00770E87 /* PreferencesWindow.swift in Sources */, 272 | A2D582281BCAAF37006A464B /* StatusMenuController.swift in Sources */, 273 | A2DEB14E1BC9980E004AAEB3 /* AppDelegate.swift in Sources */, 274 | A2B4247D1BCDE3D300887CB2 /* WeatherView.swift in Sources */, 275 | ); 276 | runOnlyForDeploymentPostprocessing = 0; 277 | }; 278 | A2DEB1551BC9980E004AAEB3 /* Sources */ = { 279 | isa = PBXSourcesBuildPhase; 280 | buildActionMask = 2147483647; 281 | files = ( 282 | A2DEB15E1BC9980E004AAEB3 /* WeatherBarTests.swift in Sources */, 283 | ); 284 | runOnlyForDeploymentPostprocessing = 0; 285 | }; 286 | A2DEB1601BC9980E004AAEB3 /* Sources */ = { 287 | isa = PBXSourcesBuildPhase; 288 | buildActionMask = 2147483647; 289 | files = ( 290 | A2DEB1691BC9980E004AAEB3 /* WeatherBarUITests.swift in Sources */, 291 | ); 292 | runOnlyForDeploymentPostprocessing = 0; 293 | }; 294 | /* End PBXSourcesBuildPhase section */ 295 | 296 | /* Begin PBXTargetDependency section */ 297 | A2DEB15B1BC9980E004AAEB3 /* PBXTargetDependency */ = { 298 | isa = PBXTargetDependency; 299 | target = A2DEB1491BC9980E004AAEB3 /* WeatherBar */; 300 | targetProxy = A2DEB15A1BC9980E004AAEB3 /* PBXContainerItemProxy */; 301 | }; 302 | A2DEB1661BC9980E004AAEB3 /* PBXTargetDependency */ = { 303 | isa = PBXTargetDependency; 304 | target = A2DEB1491BC9980E004AAEB3 /* WeatherBar */; 305 | targetProxy = A2DEB1651BC9980E004AAEB3 /* PBXContainerItemProxy */; 306 | }; 307 | /* End PBXTargetDependency section */ 308 | 309 | /* Begin PBXVariantGroup section */ 310 | A2DEB1511BC9980E004AAEB3 /* MainMenu.xib */ = { 311 | isa = PBXVariantGroup; 312 | children = ( 313 | A2DEB1521BC9980E004AAEB3 /* Base */, 314 | ); 315 | name = MainMenu.xib; 316 | sourceTree = ""; 317 | }; 318 | /* End PBXVariantGroup section */ 319 | 320 | /* Begin XCBuildConfiguration section */ 321 | A2DEB16B1BC9980E004AAEB3 /* Debug */ = { 322 | isa = XCBuildConfiguration; 323 | buildSettings = { 324 | ALWAYS_SEARCH_USER_PATHS = NO; 325 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 326 | CLANG_CXX_LIBRARY = "libc++"; 327 | CLANG_ENABLE_MODULES = YES; 328 | CLANG_ENABLE_OBJC_ARC = YES; 329 | CLANG_WARN_BOOL_CONVERSION = YES; 330 | CLANG_WARN_CONSTANT_CONVERSION = YES; 331 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 332 | CLANG_WARN_EMPTY_BODY = YES; 333 | CLANG_WARN_ENUM_CONVERSION = YES; 334 | CLANG_WARN_INFINITE_RECURSION = YES; 335 | CLANG_WARN_INT_CONVERSION = YES; 336 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 337 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 338 | CLANG_WARN_UNREACHABLE_CODE = YES; 339 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 340 | CODE_SIGN_IDENTITY = "-"; 341 | COPY_PHASE_STRIP = NO; 342 | DEBUG_INFORMATION_FORMAT = dwarf; 343 | ENABLE_STRICT_OBJC_MSGSEND = YES; 344 | ENABLE_TESTABILITY = YES; 345 | GCC_C_LANGUAGE_STANDARD = gnu99; 346 | GCC_DYNAMIC_NO_PIC = NO; 347 | GCC_NO_COMMON_BLOCKS = YES; 348 | GCC_OPTIMIZATION_LEVEL = 0; 349 | GCC_PREPROCESSOR_DEFINITIONS = ( 350 | "DEBUG=1", 351 | "$(inherited)", 352 | ); 353 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 354 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 355 | GCC_WARN_UNDECLARED_SELECTOR = YES; 356 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 357 | GCC_WARN_UNUSED_FUNCTION = YES; 358 | GCC_WARN_UNUSED_VARIABLE = YES; 359 | MACOSX_DEPLOYMENT_TARGET = 10.10; 360 | MTL_ENABLE_DEBUG_INFO = YES; 361 | ONLY_ACTIVE_ARCH = YES; 362 | SDKROOT = macosx; 363 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 364 | }; 365 | name = Debug; 366 | }; 367 | A2DEB16C1BC9980E004AAEB3 /* Release */ = { 368 | isa = XCBuildConfiguration; 369 | buildSettings = { 370 | ALWAYS_SEARCH_USER_PATHS = NO; 371 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 372 | CLANG_CXX_LIBRARY = "libc++"; 373 | CLANG_ENABLE_MODULES = YES; 374 | CLANG_ENABLE_OBJC_ARC = YES; 375 | CLANG_WARN_BOOL_CONVERSION = YES; 376 | CLANG_WARN_CONSTANT_CONVERSION = YES; 377 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 378 | CLANG_WARN_EMPTY_BODY = YES; 379 | CLANG_WARN_ENUM_CONVERSION = YES; 380 | CLANG_WARN_INFINITE_RECURSION = YES; 381 | CLANG_WARN_INT_CONVERSION = YES; 382 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 383 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 384 | CLANG_WARN_UNREACHABLE_CODE = YES; 385 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 386 | CODE_SIGN_IDENTITY = "-"; 387 | COPY_PHASE_STRIP = NO; 388 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 389 | ENABLE_NS_ASSERTIONS = NO; 390 | ENABLE_STRICT_OBJC_MSGSEND = YES; 391 | GCC_C_LANGUAGE_STANDARD = gnu99; 392 | GCC_NO_COMMON_BLOCKS = YES; 393 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 394 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 395 | GCC_WARN_UNDECLARED_SELECTOR = YES; 396 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 397 | GCC_WARN_UNUSED_FUNCTION = YES; 398 | GCC_WARN_UNUSED_VARIABLE = YES; 399 | MACOSX_DEPLOYMENT_TARGET = 10.10; 400 | MTL_ENABLE_DEBUG_INFO = NO; 401 | SDKROOT = macosx; 402 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 403 | }; 404 | name = Release; 405 | }; 406 | A2DEB16E1BC9980E004AAEB3 /* Debug */ = { 407 | isa = XCBuildConfiguration; 408 | buildSettings = { 409 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 410 | COMBINE_HIDPI_IMAGES = YES; 411 | INFOPLIST_FILE = WeatherBar/Info.plist; 412 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 413 | PRODUCT_BUNDLE_IDENTIFIER = com.etsy.WeatherBar; 414 | PRODUCT_NAME = "$(TARGET_NAME)"; 415 | SWIFT_VERSION = 3.0; 416 | }; 417 | name = Debug; 418 | }; 419 | A2DEB16F1BC9980E004AAEB3 /* Release */ = { 420 | isa = XCBuildConfiguration; 421 | buildSettings = { 422 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 423 | COMBINE_HIDPI_IMAGES = YES; 424 | INFOPLIST_FILE = WeatherBar/Info.plist; 425 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 426 | PRODUCT_BUNDLE_IDENTIFIER = com.etsy.WeatherBar; 427 | PRODUCT_NAME = "$(TARGET_NAME)"; 428 | SWIFT_VERSION = 3.0; 429 | }; 430 | name = Release; 431 | }; 432 | A2DEB1711BC9980E004AAEB3 /* Debug */ = { 433 | isa = XCBuildConfiguration; 434 | buildSettings = { 435 | BUNDLE_LOADER = "$(TEST_HOST)"; 436 | COMBINE_HIDPI_IMAGES = YES; 437 | INFOPLIST_FILE = WeatherBarTests/Info.plist; 438 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 439 | PRODUCT_BUNDLE_IDENTIFIER = com.etsy.WeatherBarTests; 440 | PRODUCT_NAME = "$(TARGET_NAME)"; 441 | SWIFT_VERSION = 3.0; 442 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WeatherBar.app/Contents/MacOS/WeatherBar"; 443 | }; 444 | name = Debug; 445 | }; 446 | A2DEB1721BC9980E004AAEB3 /* Release */ = { 447 | isa = XCBuildConfiguration; 448 | buildSettings = { 449 | BUNDLE_LOADER = "$(TEST_HOST)"; 450 | COMBINE_HIDPI_IMAGES = YES; 451 | INFOPLIST_FILE = WeatherBarTests/Info.plist; 452 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 453 | PRODUCT_BUNDLE_IDENTIFIER = com.etsy.WeatherBarTests; 454 | PRODUCT_NAME = "$(TARGET_NAME)"; 455 | SWIFT_VERSION = 3.0; 456 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WeatherBar.app/Contents/MacOS/WeatherBar"; 457 | }; 458 | name = Release; 459 | }; 460 | A2DEB1741BC9980E004AAEB3 /* Debug */ = { 461 | isa = XCBuildConfiguration; 462 | buildSettings = { 463 | COMBINE_HIDPI_IMAGES = YES; 464 | INFOPLIST_FILE = WeatherBarUITests/Info.plist; 465 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 466 | PRODUCT_BUNDLE_IDENTIFIER = com.etsy.WeatherBarUITests; 467 | PRODUCT_NAME = "$(TARGET_NAME)"; 468 | SWIFT_VERSION = 3.0; 469 | TEST_TARGET_NAME = WeatherBar; 470 | USES_XCTRUNNER = YES; 471 | }; 472 | name = Debug; 473 | }; 474 | A2DEB1751BC9980E004AAEB3 /* Release */ = { 475 | isa = XCBuildConfiguration; 476 | buildSettings = { 477 | COMBINE_HIDPI_IMAGES = YES; 478 | INFOPLIST_FILE = WeatherBarUITests/Info.plist; 479 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 480 | PRODUCT_BUNDLE_IDENTIFIER = com.etsy.WeatherBarUITests; 481 | PRODUCT_NAME = "$(TARGET_NAME)"; 482 | SWIFT_VERSION = 3.0; 483 | TEST_TARGET_NAME = WeatherBar; 484 | USES_XCTRUNNER = YES; 485 | }; 486 | name = Release; 487 | }; 488 | /* End XCBuildConfiguration section */ 489 | 490 | /* Begin XCConfigurationList section */ 491 | A2DEB1451BC9980E004AAEB3 /* Build configuration list for PBXProject "WeatherBar" */ = { 492 | isa = XCConfigurationList; 493 | buildConfigurations = ( 494 | A2DEB16B1BC9980E004AAEB3 /* Debug */, 495 | A2DEB16C1BC9980E004AAEB3 /* Release */, 496 | ); 497 | defaultConfigurationIsVisible = 0; 498 | defaultConfigurationName = Release; 499 | }; 500 | A2DEB16D1BC9980E004AAEB3 /* Build configuration list for PBXNativeTarget "WeatherBar" */ = { 501 | isa = XCConfigurationList; 502 | buildConfigurations = ( 503 | A2DEB16E1BC9980E004AAEB3 /* Debug */, 504 | A2DEB16F1BC9980E004AAEB3 /* Release */, 505 | ); 506 | defaultConfigurationIsVisible = 0; 507 | defaultConfigurationName = Release; 508 | }; 509 | A2DEB1701BC9980E004AAEB3 /* Build configuration list for PBXNativeTarget "WeatherBarTests" */ = { 510 | isa = XCConfigurationList; 511 | buildConfigurations = ( 512 | A2DEB1711BC9980E004AAEB3 /* Debug */, 513 | A2DEB1721BC9980E004AAEB3 /* Release */, 514 | ); 515 | defaultConfigurationIsVisible = 0; 516 | defaultConfigurationName = Release; 517 | }; 518 | A2DEB1731BC9980E004AAEB3 /* Build configuration list for PBXNativeTarget "WeatherBarUITests" */ = { 519 | isa = XCConfigurationList; 520 | buildConfigurations = ( 521 | A2DEB1741BC9980E004AAEB3 /* Debug */, 522 | A2DEB1751BC9980E004AAEB3 /* Release */, 523 | ); 524 | defaultConfigurationIsVisible = 0; 525 | defaultConfigurationName = Release; 526 | }; 527 | /* End XCConfigurationList section */ 528 | }; 529 | rootObject = A2DEB1421BC9980E004AAEB3 /* Project object */; 530 | } 531 | -------------------------------------------------------------------------------- /docs/params.json: -------------------------------------------------------------------------------- 1 | {"name":"WeatherBar","tagline":"Demo weather app for Etsy School class on writing a Mac app in Swift","body":"# Write a Mac Menu Bar App in Swift\r\n\r\nThis tutorial will walk you through writing a Mac Menu Bar (aka Status Bar) app, using Swift.\r\n\r\n### Create the Project\r\n- Open Xcode\r\n- Create a New Project or File -> New -> Project\r\n- Choose Application -> Cocoa Application under OS X and click Next\r\n- Product Name: WeatherBar, Language Swift, uncheck Use Storyboards\r\n- Next and save somewhere\r\n\r\n### Let's Code!\r\n\r\n- Click on MainMenu.xib\r\n- Under Objects, delete the default window and menu\r\n- Go to the library, type \"menu\" and drag out an NSMenu\r\n- Delete all but one item\r\n- rename item to Quit. In Attributes Inspector (⌥⌘4), click on the Key Equivalent field and type ⌘Q\r\n- Open the Assistant Editor (⌥⌘↩)\r\n- ctrl-drag from Menu to code (AppDelegate.swift) and create a statusMenu outlet\r\n- ctrl-drag from the Quit menu item to the code and create a quitClicked action (set type to NSMenuItem)\r\n- in AppDelegate.swift:\r\n - delete the `window` var\r\n - under `statusMenu`, add:\r\n\r\n```swift\r\nlet statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-1) // NSVariableStatusItemLength\r\n```\r\n\r\n(The `NSVariableStatusItemLength` (-1) and `NSSquareStatusItemLength` (-2) constants have not been ported over to Swift yet.)\r\n - in `applicationDidFinishLaunching`, add:\r\n\r\n```swift\r\nstatusItem.title = \"WeatherBar\"\r\nstatusItem.menu = statusMenu\r\n```\r\n\r\n- in `quitClicked`:\r\n\r\n NSApplication.sharedApplication().terminate(self)\r\n\r\n- run it\r\n\r\n## Get rid of the dock icon and menu\r\n\r\n- click target, then info\r\n- in properties, add new property (click on the last property and then on the + that appears)\r\n- type \"Application is agent (UIElement)\" and set the value to YES\r\n- run again\r\n\r\n## Create an icon\r\n\r\n- Create the icon\r\n + have two icons, one 18x18 and one 36x36\r\n + click Images.xcassets, then plus on the bottom of the next panel to the right, and select new image set\r\n + name the image set \"statusIcon\" and drag the icons into the 1x and 2x boxes\r\n\r\n\r\n- in `applicationDidFinishLaunching`:\r\n\r\n let icon = NSImage(named: \"statusIcon\")\r\n icon?.setTemplate(true) // best for dark mode\r\n statusItem.image = icon\r\n statusItem.menu = statusMenu\r\n\r\n- delete the statusItem.title line\r\n\r\n- run again\r\n\r\n## Reorganize\r\n\r\nBefore we add more code, we should find a better place to put it. The ApplicationDelegate is really meant to be used only for handling application lifecycle events. We *could* dump all our code in there, but at some point you're going to hate yourself (or the next developer to work on your code will be thinking stabby thoughts).\r\n\r\n- File -> New File -> Source -> Swift File -> \"StatusMenuController\"\r\n\r\n import Foundation\r\n import Cocoa\r\n\r\n class StatusMenuController: NSObject {\r\n @IBOutlet weak var statusMenu: NSMenu!\r\n\r\n let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-1) // NSVariableStatusItemLength\r\n\r\n override func awakeFromNib() {\r\n let icon = NSImage(named: \"statusIcon\")\r\n icon?.setTemplate(true) // best for dark mode\r\n statusItem.image = icon\r\n statusItem.menu = statusMenu\r\n }\r\n\r\n @IBAction func quitClicked(sender: NSMenuItem) {\r\n NSApplication.sharedApplication().terminate(self)\r\n }\r\n }\r\n\r\n- Go to MainMenu.xib\r\n- In the Library, type \"object\", and then drag an Object over to just above your Menu.\r\n- Name the Object \"StatusMenuController\", select the Identity Inspector (⌥⌘3), and enter \"StatusMenuController\" in the Class field\r\n- right-click on the StatusMenuController object, and under Outlets, drag the circle next to statusMenu over to your Menu object.\r\n- do that again for the quit-clicked action, going to your Quit menu item\r\n- finally, right-click on the App Delegate object and click the X next to the statusMenu outlet to clear that association.\r\n- Now, when the application is launched and the StatusMenu.xib is instantiated, our StatusMenuController will receive `awakeFromNib`, and we can do what we need to initialize the status menu.\r\n- Delete the code we added to AppDelegate\r\n\r\n## Calling the API\r\n\r\nThe next thing we need is something to manage communication with the weather API\r\n\r\n- File -> New File -> Source -> Swift File -> WeatherAPI.swift\r\n\r\n```swift\r\nclass WeatherAPI {\r\n let BASE_URL = \"http://api.openweathermap.org/data/2.5/weather?units=imperial&q=\"\r\n\r\n func fetchWeather(query: String) {\r\n let session = NSURLSession.sharedSession()\r\n let escapedQuery = query.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())\r\n let url = NSURL(string: BASE_URL + escapedQuery!)\r\n let task = session.dataTaskWithURL(url!) { data, response, error in\r\n let dataString = NSString(data: data, encoding: NSUTF8StringEncoding) as String\r\n NSLog(dataString)\r\n }\r\n task.resume()\r\n }\r\n}\r\n```\r\n\r\nNow we need a way to call this. We could just stick a call in AppDelegate or StatusMenuController#awakeFromNib, but lets be a little less lazy and add a menu item to call it.\r\n\r\n- in MainMenu.xib, type \"Menu Item\" into the library search field (bottom right), and drag a menuItem over to above Quit in your menu\r\n- while we're at it, drag a Separator Menu Item between those two\r\n- Rename the new menu item \"Update\" (and give it a key equivalent if you want)\r\n- Open the Assistant Editor with StatusMenuController.swift and ctrl-drag from Update over to your code above `quitClicked` and create a new action, `updateClicked`, with the type again as `NSMenuItem`\r\n- we need to instantiate WeatherAPI, so in StatusMenuController at the top, under `let statusItem` add:\r\n\r\n```swift\r\nlet weatherAPI = WeatherAPI()\r\n```\r\n\r\nand in `updateClicked`, add:\r\n\r\n```swift\r\nweatherAPI.fetchWeather(\"Seattle\")\r\n```\r\n\r\n- run it, and select Update\r\n\r\n- you probably want it to fetch the weather as soon as the app launches. I reorganized my `StatusMenuController` a bit to do this. Here's what it looks like now:\r\n\r\n```swift\r\nclass StatusMenuController: NSObject {\r\n @IBOutlet weak var statusMenu: NSMenu!\r\n\r\n let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-1) // NSVariableStatusItemLength\r\n let weatherAPI = WeatherAPI()\r\n\r\n override func awakeFromNib() {\r\n let icon = NSImage(named: \"statusIcon\")\r\n icon?.setTemplate(true) // best for dark mode\r\n statusItem.image = icon\r\n statusItem.menu = statusMenu\r\n\r\n updateWeather()\r\n }\r\n\r\n func updateWeather() {\r\n weatherAPI.fetchWeather(\"Seattle\")\r\n }\r\n\r\n @IBAction func updateClicked(sender: NSMenuItem) {\r\n updateWeather()\r\n }\r\n\r\n @IBAction func quitClicked(sender: NSMenuItem) {\r\n NSApplication.sharedApplication().terminate(self)\r\n }\r\n}\r\n```\r\n\r\n## Parsing JSON\r\n\r\nParsing JSON is a little awkward in Swift, and people have written libraries, like [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON) to make this easier, but our needs our simple and I don't want to complicate things with installing external libraries (although if you do, the two main package managers for Xcode are [Carthage](https://github.com/Carthage/Carthage) and CocoaPODS(http://cocoapods.org/)).\r\n\r\nHere's the JSON returned by OpenWeatherMap:\r\n\r\n```json\r\n{\r\n \"coord\": {\r\n \"lon\": -122.33,\r\n \"lat\": 47.6\r\n },\r\n \"sys\": {\r\n \"type\": 1,\r\n \"id\": 2923,\r\n \"message\": 0.0242,\r\n \"country\": \"United States of America\",\r\n \"sunrise\": 1426774374,\r\n \"sunset\": 1426818056\r\n },\r\n \"weather\": [{\r\n \"id\": 800,\r\n \"main\": \"Clear\",\r\n \"description\": \"sky is clear\",\r\n \"icon\": \"01d\"\r\n }],\r\n \"base\": \"cmc stations\",\r\n \"main\": {\r\n \"temp\": 52.41,\r\n \"pressure\": 1020,\r\n \"humidity\": 76,\r\n \"temp_min\": 48.2,\r\n \"temp_max\": 57\r\n },\r\n \"wind\": {\r\n \"speed\": 7.78,\r\n \"deg\": 180\r\n },\r\n \"clouds\": {\r\n \"all\": 1\r\n },\r\n \"dt\": 1426790612,\r\n \"id\": 5809844,\r\n \"name\": \"Seattle\",\r\n \"cod\": 200\r\n}\r\n```\r\n\r\nThere's a lot of information we could use here, but for now let's just take the city name, current temperature, and the weather description. Let's first create a place to put the weather data. In WeatherAPI.swift, add a struct at the top of the file:\r\n\r\n```swift\r\nstruct Weather {\r\n var city: String\r\n var currentTemp: Float\r\n var conditions: String\r\n}\r\n```\r\n\r\nNow add a function to parse the incoming JSON data and return a Weather object:\r\n\r\n```swift\r\nfunc weatherFromJSONData(data: NSData) -> Weather? {\r\n var err: NSError?\r\n typealias JSONDict = [String:AnyObject]\r\n\r\n if let json = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.allZeros, error: &err) as? JSONDict {\r\n var mainDict = json[\"main\"] as JSONDict\r\n var weatherList = json[\"weather\"] as [JSONDict]\r\n var weatherDict = weatherList[0]\r\n\r\n var weather = Weather(\r\n city: json[\"name\"] as String,\r\n currentTemp: mainDict[\"temp\"] as Float,\r\n conditions: weatherDict[\"main\"] as String\r\n )\r\n\r\n return weather\r\n }\r\n return nil\r\n}\r\n```\r\n\r\nWe return an Optional(Weather) because it's possible the JSON may fail to parse.\r\n\r\nNow, change the `fetchWeather` function to call `weatherFromJSONData`:\r\n\r\n```swift\r\nlet task = session.dataTaskWithURL(url!) { data, response, error in\r\n let weather = self.weatherFromJSONData(data)\r\n NSLog(\"\\(weather)\")\r\n}\r\n```\r\n\r\nIf you run it now, you'll see that the logging isn't terribly helpful:\r\n\r\n```\r\n2015-03-19 14:58:00.758 WeatherBar[49688:1998824] Optional(WeatherBar.Weather)\r\n```\r\n\r\nTo make our Weather struct printable, we need to implement the [Printable](https://developer.apple.com/library/ios/documentation/General/Reference/SwiftStandardLibraryReference/Printable.html) or DebugPrintable protocols. Let's do the former:\r\n\r\n```swift\r\nstruct Weather: Printable {\r\n var city: String\r\n var currentTemp: Float\r\n var conditions: String\r\n\r\n var description: String {\r\n return \"\\(city): \\(currentTemp)F and \\(conditions)\"\r\n }\r\n}\r\n```\r\n\r\nIf you run it again now you'll see:\r\n\r\n```\r\n2015-03-19 15:11:49.130 WeatherBar[50731:2009152] Optional(Seattle: 58.87F and Clouds)\r\n```\r\n\r\n## Getting the Weather into the Controller\r\n\r\nNext, let's actually display the weather in our app, as opposed to in the debug console.\r\n\r\nFirst we have the problem of how we get the weather data back into our controller. The weather API call is asynchronous, so we can't just call weatherAPI.fetchWeather() and expect a Weather object in return.\r\n\r\nThere are two common ways to handle this. The most common pattern in MacOS and iOS programming (at least up until recently), is to use a delegate:\r\n\r\nAdd the following above `class WeatherAPI`:\r\n\r\n```swift\r\nprotocol WeatherAPIDelegate {\r\n func weatherDidUpdate(weather: Weather)\r\n}\r\n```\r\n\r\nAdd the following class variable to WeatherAPI:\r\n\r\n```swift\r\nvar delegate: WeatherAPIDelegate?\r\n```\r\n\r\nAdd an initializer fuction:\r\n\r\n```swift\r\ninit(delegate: WeatherAPIDelegate) {\r\n self.delegate = delegate\r\n}\r\n```\r\n\r\nAnd now the data fetch task in `fetchWeather` will look like this:\r\n\r\n```swift\r\nlet task = session.dataTaskWithURL(url!) { data, response, error in\r\n if let weather = self.weatherFromJSONData(data) {\r\n self.delegate?.weatherDidUpdate(weather)\r\n }\r\n}\r\n```\r\n\r\nFinally, we implement the `WeatherAPIDelegate` protocol in the controller, with a few changes noted:\r\n\r\n```swift\r\nclass StatusMenuController: NSObject, WeatherAPIDelegate {\r\n...\r\n var weatherAPI: WeatherAPI!\r\n\r\n override func awakeFromNib() {\r\n ...\r\n weatherAPI = WeatherAPI(delegate: self)\r\n updateWeather()\r\n }\r\n ...\r\n func weatherDidUpdate(weather: Weather) {\r\n NSLog(weather.description)\r\n }\r\n ...\r\n```\r\n\r\nHowever, with the relatively recent introduction of blocks to Objective-C, and Swift's first-class functions, a simpler way is to use callbacks:\r\n\r\n```swift\r\nfunc fetchWeather(query:String, success: (Weather) -> Void) {\r\n let session = NSURLSession.sharedSession()\r\n let escapedQuery = query.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())\r\n let url = NSURL(string: BASE_URL + escapedQuery!)\r\n let task = session.dataTaskWithURL(url!) { data, response, error in\r\n if let weather = self.weatherFromJSONData(data) {\r\n success(weather)\r\n }\r\n }\r\n task.resume()\r\n}\r\n```\r\n\r\nHere, `success` is a function that takes a Weather object as a parameter and returns `Void` (nothing).\r\n\r\nIn our controller:\r\n\r\n```swift\r\nfunc updateWeather() {\r\n weatherAPI.fetchWeather(\"Seattle, WA\") { weather in\r\n NSLog(weather.description)\r\n }\r\n}\r\n```\r\n\r\n## Displaying the Weather\r\n\r\nFinally, we'll update our menu to display the weather.\r\n\r\nIn MainMenu.xib, add a new MenuItem between Update and Quit (and another separator) and rename it \"Weather\".\r\n\r\n![](assets/weather-menu-item.png)\r\n\r\nIn your controller, in `updateWeather`, replace the `NSLog` with:\r\n\r\n```swift\r\nif let weatherMenuItem = self.statusMenu.itemWithTitle(\"Weather\") {\r\n weatherMenuItem.title = weather.description\r\n}\r\n```\r\n\r\nRun and voila!\r\n\r\nThe weather is greyed out because we have no action associated with selecting it. We could have it open a web page to a detailed forecast, but instead next we'll make a nicer display.\r\n\r\n## Creating a Weather UIView\r\n\r\nOpen MainMenu.xib.\r\n\r\nDrag a Custom View onto the page.\r\n\r\nDrag a Image View into the upper left corner of the view, and in the Image View's Size Inspector, set the width and height to 50.\r\n\r\nAdd Labels for city and current temperature/conditions (we'll use one label for both temperature and conditions).\r\n\r\nAdjust the view size down to about 265 x 90 (you can set that in the Image View's Size Inspector). It should look roughly like this:\r\n\r\n![](assets/image-view.png)\r\n\r\nNew File -> Source -> Cocoa Class, name it WeatherView and make it a subclass of NSView, and save. The file will contain a stub `drawRect` method which you can delete.\r\n\r\nBack in MainMenu.xib, click on the View, and in the Identity Inspector, set the class to \"WeatherView\". Now use the Assistant editor to bring up the xib and class file side-by-side, and then ctrl-drag from the xib to create outlets for each of the elements in the view. WeatherView.swift should look like:\r\n\r\n```swift\r\nimport Cocoa\r\n\r\nclass WeatherView: NSView {\r\n @IBOutlet weak var imageView: NSImageView!\r\n @IBOutlet weak var cityTextField: NSTextField!\r\n @IBOutlet weak var currentConditionsTextField: NSTextField!\r\n}\r\n```\r\n\r\nNow add a method to WeatherView so we can update it with a Weather object:\r\n\r\n```swift\r\n func update(weather: Weather) {\r\n cityTextField.stringValue = weather.city\r\n currentConditionsTextField.stringValue = \"\\(Int(weather.currentTemp))°F and \\(weather.conditions)\"\r\n }\r\n```\r\n\r\nNow bring up StatusMenuController in the Assistant editor, and ctrl-drag from the Weather View object over to the top of the StatusMenuController class to create a `weatherView` outlet. While we're there, we're going to add a `weatherMenuItem` class var:\r\n\r\n```swift\r\nclass StatusMenuController: NSObject {\r\n @IBOutlet weak var statusMenu: NSMenu!\r\n @IBOutlet weak var weatherView: WeatherView!\r\n var weatherMenuItem: NSMenuItem!\r\n ...\r\n```\r\n\r\nIn StatusMenuController's `awakeFromNib` method, right before the call to `updateWeather`, add:\r\n\r\n```swift\r\n// load WeatherView\r\nweatherMenuItem = statusMenu.itemWithTitle(\"Weather\")\r\nweatherMenuItem.view = weatherView\r\n```\r\n\r\nAnd now `updateWeather` is even simpler:\r\n\r\n```swift\r\nfunc updateWeather() {\r\n weatherAPI.fetchWeather(\"Seattle, WA\") { weather in\r\n self.weatherView.update(weather)\r\n }\r\n}\r\n```\r\n\r\nRun it!\r\n\r\n## Adding the Weather Image\r\n\r\nSo, we're obviously missing something in our weather view. Let's update it with the appropriate weather image.\r\n\r\nThe images for the various weather conditions can be found at http://openweathermap.org/weather-conditions, but I've put them in a [zip file](assets/weather-icons.zip) for you. You can just unzip that and drag the whole folder into Images.xcassets.\r\n\r\nWe need to update WeatherAPI to capture the icon code. In the Weather struct, add:\r\n\r\n```swift\r\nvar icon: String\r\n```\r\n\r\nand in `weatherFromJSONData`, add that to the Weather initialization:\r\n\r\n```swift\r\nvar weather = Weather(\r\n city: json[\"name\"] as String,\r\n currentTemp: mainDict[\"temp\"] as Float,\r\n conditions: weatherDict[\"main\"] as String,\r\n icon: weatherDict[\"icon\"] as String\r\n)\r\n```\r\n\r\nNow in the `update` method of WeatherView, add:\r\n\r\n```swift\r\nimageView.image = NSImage(named: weather.icon)\r\n```\r\n\r\nThat's it! Run it.\r\n\r\n## Preferences\r\n\r\nHaving the city hard-coded in the app is not cool. Let's make a Preferences pane so we can change it.\r\n\r\nOpen up MainMenu.xib and drag another MenuItem onto the menu, above Quit, naming it \"Preferences...\".\r\n\r\nOpen up the Assistant editor again with StatusMenuController, and ctrl-drag from the Preferences menu item over to the code and create a \"preferencesClicked\" action.\r\n\r\nNew -> File -> Source -> Cocoa Class. Call it \"PreferencesWindow\", set the subclass to NSWindowController, and check the box to create a XIB file.\r\n\r\nGive the window a title of Preferences. Add a label for \"City:\", and put a Text Field to the right of it. It should look something like this:\r\n\r\n![](assets/preferences.png)\r\n\r\nBring up the Assistant editor with PreferencesWindow.swift and ctrl-drag from the text field to the code and create an outlet named \"cityTextField\".\r\n\r\nIn PreferencesWindow.swift, add:\r\n\r\n```swift\r\noverride var windowNibName : String! {\r\n return \"PreferencesWindow\"\r\n}\r\n```\r\n\r\nand at the end of `windowDidLoad()`, add:\r\n\r\n```swift\r\nself.window?.center()\r\n```\r\n\r\nIn StatusMenuController.swift, add a `preferencesWindow` class var:\r\n\r\n```swift\r\nvar preferencesWindow: PreferencesWindow!\r\n```\r\n\r\nand initialize in `awakeFromNib()`, before the call to `updateWeather()`:\r\n\r\n```swift\r\npreferencesWindow = PreferencesWindow()\r\n```\r\n\r\nFinally, in the `preferencesClicked` function, add:\r\n\r\n```swift\r\npreferencesWindow.showWindow(nil)\r\n```\r\n\r\nIf you run now, selecting the Preferences... menu item should bring up the preferences window.\r\n\r\nNow, let's actually save and update the city.\r\n\r\nMake the PreferencesWindow class an `NSWindowDelegate`:\r\n\r\n```swift\r\nclass PreferencesWindow: NSWindowController, NSWindowDelegate {\r\n```\r\n\r\nand add:\r\n\r\n```swift\r\nfunc windowWillClose(notification: NSNotification) {\r\n NSLog(\"city is: \" + cityTextField.stringValue)\r\n}\r\n```\r\n\r\nIf you run it now, you'll see whatever you typed in the text field displayed when you close the window.\r\n\r\nSaving the value is easy:\r\n\r\n```swift\r\nfunc windowWillClose(notification: NSNotification) {\r\n let defaults = NSUserDefaults.standardUserDefaults()\r\n defaults.setValue(cityTextField.stringValue, forKey: \"city\")\r\n}\r\n```\r\n\r\nNow we need to notify the StatusMenuController that the preferences have been updated. For this we'll use the Delegate pattern. This is easy, but requires a number of edits. First, at the top of PreferencesWindow.swift, add a `PreferencesWindowDelegate` protocol:\r\n\r\n```swift\r\nprotocol PreferencesWindowDelegate {\r\n func preferencesDidUpdate()\r\n}\r\n```\r\n\r\nand add a `delegate` instance variable:\r\n\r\n```swift\r\nvar delegate: PreferencesWindowDelegate?\r\n```\r\n\r\nAt the end of `windowWillClose`, we'll call the delegate:\r\n\r\n```swift\r\ndelegate?.preferencesDidUpdate()\r\n```\r\n\r\nBack in StatusMenuController, make it a `PreferencesWindowDelegate`:\r\n\r\n```swift\r\nclass StatusMenuController: NSObject, PreferencesWindowDelegate {\r\n```\r\n\r\nand add the delegate method:\r\n\r\n```swift\r\nfunc preferencesDidUpdate() {\r\n updateWeather()\r\n}\r\n```\r\n\r\nAnd in `awakeFromNib`, set the delegate:\r\n\r\n```swift\r\npreferencesWindow = PreferencesWindow()\r\npreferencesWindow.delegate = self\r\n```\r\n\r\nAll that's left is to load the city from defaults. First add this at the top of StatusMenuController, under the imports:\r\n\r\n```swift\r\nlet DEFAULT_CITY = \"Seattle, WA\"\r\n```\r\n\r\n(...or whatever you want the default to be.) Yes, this is a global variable, and there are probably better ways to do this (like storing it in Info.plist), but that can be left as an exercise for the reader.\r\n\r\nLoad the saved city, or default, in `updateWeather`:\r\n\r\n```swift\r\nfunc updateWeather() {\r\n let defaults = NSUserDefaults.standardUserDefaults()\r\n let city = defaults.stringForKey(\"city\") ?? DEFAULT_CITY\r\n weatherAPI.fetchWeather(city) { weather in\r\n self.weatherView.update(weather)\r\n }\r\n}\r\n```\r\n\r\nFinally, back in PreferencesWindow.swift, we need to add similar code to load any saved city when we show the preferences. At the end of `windowDidLoad`, add:\r\n\r\n```swift\r\nlet defaults = NSUserDefaults.standardUserDefaults()\r\nlet city = defaults.stringForKey(\"city\") ?? DEFAULT_CITY\r\ncityTextField.stringValue = city\r\n```\r\n\r\nRun it!\r\n\r\n## Next Steps\r\n\r\nThat's the end of this tutorial. Obviously there's a lot more that we can do with this, but I'll leave that up to you. Some ideas:\r\n\r\n- Easy\r\n + Add other weather info (high/low temp, humidity, sunrise/sunset, etc) to the Weather View\r\n + Change the status menu icon + title to reflect the current conditions\r\n + Make it so clicking on the Weather View opens a browser with detailed weather information (easy, if you have a url to go to; hint: `NSWorkspace.sharedWorkspace().openURL(url: NSURL)`)\r\n- More Challenging\r\n + Add support for multiple cities. This will take some effort, especially if the number of cities is dynamic. I think you'll have to put the Weather View in its own XIB, and load it manually (look at `NSBundle.mainBundle().loadNibNamed(name, owner: owner, options: options)`). The UI in Preferences will need to be updated as well.\r\n- You Know Way More Than Me Now\r\n + Create a completely custom view when clicking on the app in the status bar. See the [Weather Live](https://itunes.apple.com/us/app/weather-live/id755717884?mt=12) app, for example. I haven't tried this, but I suspect it is easier than you might think (depending on how fancy your view is, of course).\r\n\r\n## Resources\r\n\r\n- [The Swift Programming Language](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/)\r\n + Apple's documentation, also downloadable as a [free iBook](https://itunes.apple.com/us/book/the-swift-programming-language/id881256329?mt=11)\r\n- [Mac Dev Center](https://developer.apple.com/devcenter/mac/)\r\n + Mac developer account is free, but you need to pay $99/year if you want to distribute your app in the app store.\r\n- [OS X Human Interface Guidelines](https://developer.apple.com/library/mac/documentation/UserExperience/Conceptual/OSXHIGuidelines/)","google":"","note":"Don't delete this file! It's used internally to help with page regeneration."} -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | **Update (March 4, 2017):** Tutorial updated for Swift 3 and Xcode 8.2.1. 6 | 7 | This tutorial will walk you through writing a Mac Menu Bar (aka Status Bar) app, using [Swift](https://developer.apple.com/swift/). We'll build a simple weather app using data from [OpenWeatherMap](http://openweathermap.org/). In the end, it will look like this: 8 | 9 | ![](assets/weatherbar.png) 10 | 11 | The complete code can be found at . 12 | 13 | ## What's a Menu Bar App and Why Would I Want to Write One? 14 | 15 | Technically, they're Status Bar apps, but most people outside of Apple will refer to them as Menu Bar apps. 16 | 17 | {% include image.html url="assets/squeeler.png" description="Squeeler, an app I wrote to warn of processes using excessive CPU." %} 18 | 19 | Menu Bar apps are great for creating quick utilities. Because the UI is fairly constrained, they are easy to design and create. They're especially well-suited for processes that run in the background, or for quick reference. 20 | 21 | ## Prerequisites 22 | 23 | You should have the latest stable version of [Xcode](https://itunes.apple.com/us/app/xcode/id497799835?mt=12). At the time of this writing, that is 8.2.1. As Swift is a language still under heavy development, it's not unlikely that the code written here may fail to compile on earlier or later versions. I'll try to keep this up-to-date, but if you find any errors, please [create an issue](https://github.com/bgreenlee/WeatherBar/issues). 24 | 25 | ## Let's Get Something Running 26 | 27 | Open Xcode. 28 | 29 | Create a New Project or *File ⟶ New ⟶ Project* 30 | 31 | Under "Choose a template for your new project", select "macOS", and then in the Application section, select "Cocoa." Click Next. 32 | 33 | ![New project, step 1](assets/new-project-1.png) 34 | 35 | Product Name: *WeatherBar*, Language *Swift*, make sure *Use Storyboards* is unchecked. Team, organization name and identifier are not important at this stage. Make something up if you like. You can check "Include Unit Tests" and "Include UI Tests" if you want, but we won't be covering those in this tutorial. 36 | 37 | ![New project, step 2](assets/new-project-2.png) 38 | 39 | Hit next and save somewhere. 40 | 41 | ### Let's Code! 42 | 43 | Click on *MainMenu.xib* in the sidebar. 44 | 45 | Under Objects, delete the default window and menu. 46 | 47 | ![Delete main menu and default window](assets/window-menu-delete.png) 48 | 49 | Note: if you don't see Main Menu and Window in the second pane from the left, make that pane wider by dragging the right side of it to the right. 50 | 51 | Go to the Object Library (right-hand pane, on the bottom; if it isn't showing, go to the menu item View ⟶ Utilities ⟶ Show Object Library, or hit ⌃⌥⌘3). Type "menu" and drag a *Menu* object over to your Objects list, under Font Manager. 52 | 53 | ![Dragging a Menu object](assets/add-menu.gif) 54 | 55 | Delete all but one menu item. 56 | 57 | Rename the last menu item to *Quit*. 58 | 59 | Open the Assistant Editor (⌥⌘↩); this lets us have the Interface Builder and our code side-by-side (if you're a bit cramped for space, you can toggle the Utilities pane on the far right by hitting ⌥⌘0). Make sure *AppDelegate.swift* is in the right-hand pane. 60 | 61 | Hold down the control key and drag from your Menu object over to the code, just above or below where it says `@IBOutlet weak var window: NSWindow!`. "Insert Outlet" will appear. Let go and a dialog will pop up asking you to name the outlet. Call it `statusMenu` and hit Connect. 62 | 63 | ![Create statusMenu outlet](assets/create-outlet.png) 64 | 65 | Do the same for the *Quit* menu item, control-dragging to the code, but this time create an **action** named `quitClicked` **action**, with the type set to *NSMenuItem*. 66 | 67 | ![Create Quit action](assets/create-quit-action.png) 68 | 69 | Delete the `@IBOutlet weak var window: NSWindow!` line. We don't need it. 70 | 71 | In *AppDelegate.swift*, under `statusMenu`, add: 72 | 73 | ```swift 74 | let statusItem = NSStatusBar.system().statusItem(withLength: NSVariableStatusItemLength) 75 | ``` 76 | 77 | In `applicationDidFinishLaunching`, add: 78 | 79 | ```swift 80 | statusItem.title = "WeatherBar" 81 | statusItem.menu = statusMenu 82 | ``` 83 | 84 | And in `quitClicked`: 85 | 86 | ```swift 87 | NSApplication.shared().terminate(self) 88 | ``` 89 | 90 | Your code should now look like: 91 | 92 | ```swift 93 | import Cocoa 94 | 95 | @NSApplicationMain 96 | class AppDelegate: NSObject, NSApplicationDelegate { 97 | 98 | @IBOutlet weak var statusMenu: NSMenu! 99 | 100 | let statusItem = NSStatusBar.system().statusItem(withLength: NSVariableStatusItemLength) 101 | 102 | @IBAction func quitClicked(sender: NSMenuItem) { 103 | NSApplication.shared().terminate(self) 104 | } 105 | 106 | func applicationDidFinishLaunching(_ aNotification: Notification) { 107 | statusItem.title = "WeatherBar" 108 | statusItem.menu = statusMenu 109 | } 110 | 111 | func applicationWillTerminate(_ aNotification: Notification) { 112 | // Insert code here to tear down your application 113 | } 114 | 115 | } 116 | ``` 117 | 118 | Run it! Hit ⌘R or Product ⟶ Run. You now have a working menu bar app. 119 | 120 | ## Make it Look Like a Real Menu Bar App 121 | 122 | You might have noticed an icon shows up in your Dock when you ran your app. You also get the default application menus on the left side of the status bar. Menu bar apps don't need either of those, so let's get rid of them. 123 | 124 | Click the application name in the navigator on the left side, then Info in the center pane. 125 | 126 | Under *Custom OS X Application Target Properties*, add new property (click on the any property and then on the + that appears). 127 | 128 | Type "Application is agent (UIElement)" and set the value to YES. 129 | 130 | ![](assets/target-info.png) 131 | 132 | Run it again and you'll see the Dock icon and default menus are gone. 133 | 134 | ### Create an Icon 135 | 136 | For the icon that goes in the status bar, you need two versions, one 18x18 ![](assets/statusIcon-sun.png), and one 2x version at 36x36 ![](assets/statusIcon-sun@2x.png). 137 | 138 | Click _Assets.xcassets_ in the left sidebar, then the plus button on the bottom of the next panel to the right, and select _New Image Set_. Name the image set "statusIcon" and drag the icons into the 1x and 2x boxes (you can drag them straight from your browser into Xcode). 139 | 140 | ![](assets/add-status-icon.png) 141 | 142 | In `applicationDidFinishLaunching`, add: 143 | 144 | ```swift 145 | let icon = NSImage(named: "statusIcon") 146 | icon?.isTemplate = true // best for dark mode 147 | statusItem.image = icon 148 | statusItem.menu = statusMenu 149 | ``` 150 | 151 | Delete the `statusItem.title` line. 152 | 153 | Try running it again. We have an icon! 154 | 155 | You're probably wondering why the icon that shows up is just a black version of our nice yellow sun. Try commenting out the `icon?.isTemplate = true` line and running it again. Boom! Cool yellow sun. 156 | 157 | You'll notice that most, if not all, the icons in your status bar are black. The reason Apple prefers this is to make them work better in dark mode. Go to your System Preferences, General, and you'll see a checkbox at the top for "Use dark menu bar and Dock". Try checking it. 158 | 159 | Turns out our sun icon actually looks better in dark mode than in normal mode. It's really your call whether you want the black & white version or the color version. Just know that Apple prefers you use the boring version. 160 | 161 | ## Reorganize 162 | 163 | Before we add more code, we should find a better place to put it. The AppDelegate is really meant to be used only for handling application lifecycle events. We *could* dump all our code in there, but at some point you're going to hate yourself. 164 | 165 | Create a controller for the status menu: File ⟶ New File ⟶ macOS Source ⟶ Cocoa Class ⟶ Next 166 | 167 | ![](assets/new-cocoa-class.png) 168 | 169 | Name the class "StatusMenuController", with a subclass of _NSObject_. Hit next and create. Now let's move over the following code from the AppDelegate. Rename the `applicationDidFinishLaunching` method to `override func awakeFromNib()`. The StatusMenuController should look like: 170 | 171 | ```swift 172 | // StatusMenuController.swift 173 | 174 | import Cocoa 175 | 176 | class StatusMenuController: NSObject { 177 | @IBOutlet weak var statusMenu: NSMenu! 178 | 179 | let statusItem = NSStatusBar.system().statusItem(withLength: NSVariableStatusItemLength) 180 | 181 | override func awakeFromNib() { 182 | let icon = NSImage(named: "statusIcon") 183 | icon?.isTemplate = true // best for dark mode 184 | statusItem.image = icon 185 | statusItem.menu = statusMenu 186 | } 187 | 188 | @IBAction func quitClicked(sender: NSMenuItem) { 189 | NSApplication.shared().terminate(self) 190 | } 191 | } 192 | ``` 193 | 194 | Remove the above code from AppDelegate. It should now look the same as when we first started, minus the `window` var. 195 | 196 | ```swift 197 | // AppDelegate.swift 198 | 199 | import Cocoa 200 | 201 | @NSApplicationMain 202 | class AppDelegate: NSObject, NSApplicationDelegate { 203 | func applicationDidFinishLaunching(_ aNotification: Notification) { 204 | // Insert code here to initialize your application 205 | } 206 | func applicationWillTerminate(_ aNotification: Notification) { 207 | // Insert code here to tear down your application 208 | } 209 | } 210 | ``` 211 | 212 | Unfortunately, when you move code that was connected to UI elements (such as `statusItem` and `quitClicked`), Xcode loses the connection, so we'll need to reconnect them. 213 | 214 | First, though, we need to make sure that our StatusMenuController gets loaded when MainMenu.xib gets loaded. To do that, we need to add it as an Object in the XIB. 215 | 216 | Click on _MainMenu.xib_. Open the Object Library again, and type "object", and then drag an Object over to just above Status Menu. 217 | 218 | ![Drag new Object](assets/drag-object.png) 219 | 220 | Click on the Object, select the Identity Inspector (⌥⌘3), and enter "StatusMenuController" in the Class field. 221 | 222 | ![Set StatusMenuController class](assets/set-statusmenucontroller-class.png) 223 | 224 | Right-click on the _StatusMenuController_ object, and under Outlets, drag the circle next to _statusMenu_ over to your Menu object. 225 | 226 | ![Reconnect status menu](assets/reconnect-status-menu.png) 227 | 228 | Do that again for the _quitClicked_ action, going to your Quit menu item. 229 | 230 | Finally, right-click on the _Delegate_ object and click the X next to the statusMenu outlet to clear that association. 231 | 232 | ![Clear delegate status menu](assets/clear-delegate-status-menu.png) 233 | 234 | Now, when the application is launched and the _MainMenu.xib_ is instantiated, our StatusMenuController will receive `awakeFromNib`, and we can do what we need to initialize the status menu. 235 | 236 | Run it again to make sure it still works. 237 | 238 | ## Calling the API 239 | 240 | Time to get some actual weather data. We're going to use [OpenWeatherMap](http://openweathermap.org/). You'll need to [create an account](http://home.openweathermap.org/users/sign_up) to get your free API key. 241 | 242 | The next thing we need is something to manage communication with the weather API. 243 | 244 | File ⟶ New File ⟶ macOS Source ⟶ Swift File ⟶ WeatherAPI.swift, and add the following, **making sure you insert your API key**. 245 | 246 | ```swift 247 | import Foundation 248 | 249 | class WeatherAPI { 250 | let API_KEY = "your-api-key-here" 251 | let BASE_URL = "http://api.openweathermap.org/data/2.5/weather" 252 | 253 | func fetchWeather(_ query: String) { 254 | let session = URLSession.shared 255 | // url-escape the query string we're passed 256 | let escapedQuery = query.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) 257 | let url = URL(string: "\(BASE_URL)?APPID=\(API_KEY)&units=imperial&q=\(escapedQuery!)") 258 | let task = session.dataTask(with: url!) { data, response, err in 259 | // first check for a hard error 260 | if let error = err { 261 | NSLog("weather api error: \(error)") 262 | } 263 | 264 | // then check the response code 265 | if let httpResponse = response as? HTTPURLResponse { 266 | switch httpResponse.statusCode { 267 | case 200: // all good! 268 | if let dataString = String(data: data!, encoding: .utf8) { 269 | NSLog(dataString) 270 | } 271 | case 401: // unauthorized 272 | NSLog("weather api returned an 'unauthorized' response. Did you set your API key?") 273 | default: 274 | NSLog("weather api returned response: %d %@", httpResponse.statusCode, HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode)) 275 | } 276 | } 277 | } 278 | task.resume() 279 | } 280 | } 281 | ``` 282 | 283 | Now we need a way to call this. We could just stick a call in _AppDelegate_ or `StatusMenuController#awakeFromNib`, but let's be a little less lazy and add a menu item to call it. 284 | 285 | In _MainMenu.xib_, type "Menu Item" into the Object Library search field (bottom right), and drag a Menu Item over to above Quit in your menu. While we're at it, drag a Separator Menu Item between those two. Rename the new menu item "Update". 286 | 287 | ![Add Update menu item](assets/update-menu-item.png) 288 | 289 | Open the Assistant Editor with _StatusMenuController.swift_ and ctrl-drag from Update over to your code above `quitClicked` and create a new action, `updateClicked`, with the type again as _NSMenuItem_. 290 | 291 | We need to instantiate _WeatherAPI_, so in _StatusMenuController_ at the top, under `let statusItem` add: 292 | 293 | ```swift 294 | let weatherAPI = WeatherAPI() 295 | ``` 296 | 297 | and in `updateClicked`, add: 298 | 299 | ```swift 300 | weatherAPI.fetchWeather("Seattle") 301 | ``` 302 | 303 | Run it, and select Update. If you are running OS X 10.11 (El Capitan) or later, you will see an error in your console: `The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.` This is because OpenWeatherMap only provides (as of this writing) a non-SSL (http) endpoint, and El Capitan introduced a security measure that prevents connections to non-SSL endpoints without an explicit exception. To get past this, we need to add the exception to our _Info.plist_ file. 304 | 305 | This will be easier to do (and explain) by editing the raw XML of the _Info.plist_ file directly, instead of using the properties editor. So *right-click* on _Info.plist_ and select Open As ⟶ Source Code: 306 | 307 | ![Open Info.plist as Source Code](assets/Info-plist-open-as-source.png) 308 | 309 | At the bottom, before the last ``, add: 310 | 311 | ```xml 312 | NSAppTransportSecurity 313 | 314 | NSExceptionDomains 315 | 316 | api.openweathermap.org 317 | 318 | NSExceptionAllowsInsecureHTTPLoads 319 | 320 | 321 | 322 | 323 | ``` 324 | 325 | Save, run again, and select Update. You should see the JSON response in the console. 326 | 327 | Now, you probably want it to fetch the weather as soon as the app launches. Let's reorganize `StatusMenuController` a bit, adding an `updateWeather` method. Here's what it looks like now: 328 | 329 | ```swift 330 | import Cocoa 331 | 332 | class StatusMenuController: NSObject { 333 | @IBOutlet weak var statusMenu: NSMenu! 334 | 335 | let statusItem = NSStatusBar.system().statusItem(withLength: NSVariableStatusItemLength) 336 | let weatherAPI = WeatherAPI() 337 | 338 | override func awakeFromNib() { 339 | statusItem.menu = statusMenu 340 | let icon = NSImage(named: "statusIcon") 341 | icon?.isTemplate = true // best for dark mode 342 | statusItem.image = icon 343 | 344 | updateWeather() 345 | } 346 | 347 | func updateWeather() { 348 | weatherAPI.fetchWeather("Seattle") 349 | } 350 | 351 | @IBAction func updateClicked(_ sender: NSMenuItem) { 352 | updateWeather() 353 | } 354 | 355 | @IBAction func quitClicked(sender: NSMenuItem) { 356 | NSApplication.shared().terminate(self) 357 | } 358 | } 359 | ``` 360 | 361 | ## Parsing JSON 362 | 363 | Parsing JSON is a little awkward in Swift, and people have written libraries–like [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)–to make this easier, but our needs are simple and I don't want to complicate things with installing external libraries (although if you do, the two main package managers for Xcode are [Carthage](https://github.com/Carthage/Carthage) and [CocoaPODS](http://cocoapods.org/)). 364 | 365 | Here's the [prettified] JSON returned by OpenWeatherMap: 366 | 367 | ```json 368 | { 369 | "coord": { 370 | "lon": -122.33, 371 | "lat": 47.61 372 | }, 373 | "weather": [{ 374 | "id": 800, 375 | "main": "Clear", 376 | "description": "sky is clear", 377 | "icon": "01n" 378 | }], 379 | "base": "cmc stations", 380 | "main": { 381 | "temp": 57.45, 382 | "pressure": 1018, 383 | "humidity": 59, 384 | "temp_min": 53.6, 385 | "temp_max": 62.6 386 | }, 387 | "wind": { 388 | "speed": 2.61, 389 | "deg": 19.5018 390 | }, 391 | "clouds": { 392 | "all": 1 393 | }, 394 | "dt": 1444623405, 395 | "sys": { 396 | "type": 1, 397 | "id": 2949, 398 | "message": 0.0065, 399 | "country": "US", 400 | "sunrise": 1444659833, 401 | "sunset": 1444699609 402 | }, 403 | "id": 5809844, 404 | "name": "Seattle", 405 | "cod": 200 406 | } 407 | ``` 408 | 409 | There's a lot of information we could use here, but for now let's keep it simple and just take the city name, current temperature, and the weather description. Let's first create a place to put the weather data. In _WeatherAPI.swift_, add a struct at the top of the file: 410 | 411 | ```swift 412 | struct Weather { 413 | var city: String 414 | var currentTemp: Float 415 | var conditions: String 416 | } 417 | ``` 418 | 419 | Now, in the _WeatherAPI_ class, add a function to parse the incoming JSON data and return a Weather object: 420 | 421 | ```swift 422 | func weatherFromJSONData(_ data: Data) -> Weather? { 423 | typealias JSONDict = [String:AnyObject] 424 | let json : JSONDict 425 | 426 | do { 427 | json = try JSONSerialization.jsonObject(with: data, options: []) as! JSONDict 428 | } catch { 429 | NSLog("JSON parsing failed: \(error)") 430 | return nil 431 | } 432 | 433 | var mainDict = json["main"] as! JSONDict 434 | var weatherList = json["weather"] as! [JSONDict] 435 | var weatherDict = weatherList[0] 436 | 437 | let weather = Weather( 438 | city: json["name"] as! String, 439 | currentTemp: mainDict["temp"] as! Float, 440 | conditions: weatherDict["main"] as! String 441 | ) 442 | 443 | return weather 444 | } 445 | ``` 446 | 447 | We return an _Optional(Weather)_ because it's possible the JSON may fail to parse. 448 | 449 | Now, change the `fetchWeather` function to call `weatherFromJSONData`: 450 | 451 | ```swift 452 | let task = session.dataTask(with: url!) { data, response, err in 453 | // first check for a hard error 454 | if let error = err { 455 | NSLog("weather api error: \(error)") 456 | } 457 | 458 | // then check the response code 459 | if let httpResponse = response as? HTTPURLResponse { 460 | switch httpResponse.statusCode { 461 | case 200: // all good! 462 | if let weather = self.weatherFromJSONData(data!) { 463 | NSLog("\(weather)") 464 | } 465 | case 401: // unauthorized 466 | NSLog("weather api returned an 'unauthorized' response. Did you set your API key?") 467 | default: 468 | NSLog("weather api returned response: %d %@", httpResponse.statusCode, HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode)) 469 | } 470 | } 471 | } 472 | ``` 473 | 474 | If you run it now, you'll see something like this: 475 | 476 | ``` 477 | 2015-03-19 14:58:00.758 WeatherBar[49688:1998824] Optional(WeatherBar.Weather(city: "Seattle", currentTemp: 51.6, conditions: "Clouds")) 478 | ``` 479 | 480 | We can make that output a little neater by implementing the [CustomStringConvertible](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Reference/Swift_CustomStringConvertible_Protocol/index.html) protocol, which just entails adding a `description`. Change your _Weather_ struct to: 481 | 482 | ```swift 483 | struct Weather: CustomStringConvertible { 484 | var city: String 485 | var currentTemp: Float 486 | var conditions: String 487 | 488 | var description: String { 489 | return "\(city): \(currentTemp)F and \(conditions)" 490 | } 491 | } 492 | ``` 493 | 494 | If you run it again now you'll see: 495 | 496 | ``` 497 | 2017-03-01 06:13:48.255 WeatherBar[68094:14793691] Seattle: 51.6F and Clouds 498 | ``` 499 | 500 | ## Getting the Weather into the Controller 501 | 502 | Next, let's actually display the weather in our app, as opposed to in the debug console. 503 | 504 | First we have the problem of how we get the weather data back into our controller, so we can in turn insert it into the menu. The weather API call is asynchronous, so we can't just call `weatherAPI.fetchWeather()` and expect a Weather object in return. 505 | 506 | There are two common ways to handle asynchronous responses: delegates and callbacks. Up until recently, delegates were the most common pattern in MacOS and iOS programming. I'll describe how that would work in the next section. **Feel free to just read along, rather than implement, as our final code will use a callback.** 507 | 508 | ### Delegate Implementation 509 | 510 | Add the following above `class WeatherAPI` in **WeatherAPI.swift**: 511 | 512 | ```swift 513 | protocol WeatherAPIDelegate { 514 | func weatherDidUpdate(_ weather: Weather) 515 | } 516 | ``` 517 | 518 | Add the following class variable to the WeatherAPI class, below `let BASE_URL = ...`: 519 | 520 | ```swift 521 | var delegate: WeatherAPIDelegate? 522 | ``` 523 | 524 | Add an initializer fuction below that: 525 | 526 | ```swift 527 | init(delegate: WeatherAPIDelegate) { 528 | self.delegate = delegate 529 | } 530 | ``` 531 | 532 | And now the data fetch task in `fetchWeather` will look like this: 533 | 534 | ```swift 535 | let task = session.dataTask(with: url!) { data, response, err in 536 | // first check for a hard error 537 | if let error = err { 538 | NSLog("weather api error: \(error)") 539 | } 540 | 541 | // then check the response code 542 | if let httpResponse = response as? HTTPURLResponse { 543 | switch httpResponse.statusCode { 544 | case 200: // all good! 545 | if let weather = self.weatherFromJSONData(data!) { 546 | self.delegate?.weatherDidUpdate(weather) 547 | } 548 | case 401: // unauthorized 549 | NSLog("weather api returned an 'unauthorized' response. Did you set your API key?") 550 | default: 551 | NSLog("weather api returned response: %d %@", httpResponse.statusCode, HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode)) 552 | } 553 | } 554 | } 555 | ``` 556 | 557 | Finally, we implement the `WeatherAPIDelegate` protocol in the _StatusMenuController_, with a few changes noted: 558 | 559 | ```swift 560 | class StatusMenuController: NSObject, WeatherAPIDelegate { 561 | ... 562 | var weatherAPI: WeatherAPI! 563 | 564 | override func awakeFromNib() { 565 | ... 566 | weatherAPI = WeatherAPI(delegate: self) 567 | updateWeather() 568 | } 569 | ... 570 | func weatherDidUpdate(_ weather: Weather) { 571 | NSLog(weather.description) 572 | } 573 | ... 574 | ``` 575 | 576 | ### Callback Implementation 577 | 578 | With the introduction of blocks to Objective-C, and Swift's first-class functions, a simpler way is to use callbacks. (If you implemented the delegate changes above, go ahead and back them out first.) 579 | 580 | First, let's change the function definition of the `fetchWeather` method in _WeatherAPI.swift_ to accept a callback function: 581 | 582 | ```swift 583 | func fetchWeather(_ query: String, success: @escaping (Weather) -> Void) { 584 | ``` 585 | 586 | This says we expect a function which takes a Weather object as a parameter and returns nothing (Void). We need the `@escaping` because we're going to be using that function to call out of `fetchWeather`. For more information see [Escaping Closures](https://developer.apple.com/library/prerelease/content/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html#//apple_ref/doc/uid/TP40014097-CH11-ID546) in The Swift Programming Language documentation. 587 | 588 | Change the data-fetching task in`fetchWeather` to look like this: 589 | 590 | ```swift 591 | let task = session.dataTask(with: url!) { data, response, err in 592 | // first check for a hard error 593 | if let error = err { 594 | NSLog("weather api error: \(error)") 595 | } 596 | 597 | // then check the response code 598 | if let httpResponse = response as? HTTPURLResponse { 599 | switch httpResponse.statusCode { 600 | case 200: // all good! 601 | if let weather = self.weatherFromJSONData(data!) { 602 | success(weather) 603 | } 604 | case 401: // unauthorized 605 | NSLog("weather api returned an 'unauthorized' response. Did you set your API key?") 606 | default: 607 | NSLog("weather api returned response: %d %@", httpResponse.statusCode, HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode)) 608 | } 609 | } 610 | } 611 | ``` 612 | 613 | In our controller: 614 | 615 | ```swift 616 | func updateWeather() { 617 | weatherAPI.fetchWeather("Seattle, WA") { weather in 618 | NSLog(weather.description) 619 | } 620 | } 621 | ``` 622 | 623 | Run it to verify that it still works. 624 | 625 | Aside: In Swift, if the last parameter of a method is a function, you can use the above block syntactic sugar. It is the same as doing: 626 | 627 | ```swift 628 | weatherAPI.fetchWeather("Seattle, WA", success: { weather in NSLog(weather.description)}) 629 | ``` 630 | 631 | ## Displaying the Weather 632 | 633 | Finally, we'll update our menu to display the weather. 634 | 635 | In _MainMenu.xib_, add a new MenuItem between Update and Quit (and another separator) and rename it "Weather". 636 | 637 | ![Add Weather Menu Item](assets/add-weather-menu-item.png) 638 | 639 | In your controller, in `updateWeather`, replace the `NSLog` with: 640 | 641 | ```swift 642 | if let weatherMenuItem = self.statusMenu.item(withTitle: "Weather") { 643 | weatherMenuItem.title = weather.description 644 | } 645 | ``` 646 | 647 | Run and voila! 648 | 649 | The weather is greyed out because we have no action associated with selecting it. We could have it open a web page to a detailed forecast, but instead next we'll make a nicer display. 650 | 651 | ## Creating a Weather View 652 | 653 | Open MainMenu.xib. 654 | 655 | Drag a Custom View onto the page. 656 | 657 | Drag a Image View into the upper left corner of the view, and in the Image View's Size Inspector (⌥⌘5), set the width and height to 50. 658 | 659 | ![Set Image View Size](assets/image-view-size.png) 660 | 661 | Add Labels for city and current temperature/conditions (we'll use one label for both temperature and conditions). 662 | 663 | Adjust the view size down to about 265 x 90 (you can set that in the Custom View's Size Inspector). It should look roughly like this: 664 | 665 | ![Weather View](assets/weather-view.png) 666 | 667 | New File ⟶ macOS Source ⟶ Cocoa Class, name it WeatherView and make it a subclass of NSView, and save. The file will contain a stub `drawRect` method which you can delete. 668 | 669 | Back in MainMenu.xib, click on the Custom View, and in the Identity Inspector (⌥⌘3), set the class to "WeatherView". 670 | 671 | ![Set WeatherView Class](assets/weather-view-class.png) 672 | 673 | Now use the Assistant Editor (⌥⌘↩) to bring up the xib and class file side-by-side, and then ctrl-drag from the xib to create outlets for each of the elements in the view. WeatherView.swift should look like: 674 | 675 | ```swift 676 | import Cocoa 677 | 678 | class WeatherView: NSView { 679 | @IBOutlet weak var imageView: NSImageView! 680 | @IBOutlet weak var cityTextField: NSTextField! 681 | @IBOutlet weak var currentConditionsTextField: NSTextField! 682 | } 683 | ``` 684 | 685 | Now add a method to WeatherView so we can update it with a Weather object: 686 | 687 | ```swift 688 | func update(_ weather: Weather) { 689 | // do UI updates on the main thread 690 | DispatchQueue.main.async { 691 | self.cityTextField.stringValue = weather.city 692 | self.currentConditionsTextField.stringValue = "\(Int(weather.currentTemp))°F and \(weather.conditions)" 693 | } 694 | } 695 | ``` 696 | 697 | (The reason we use `DispatchQueue.main.async` here is that any updates to the UI should be donw on the main thread, and `update` is getting called from a networking thread.) Now bring up StatusMenuController in the Assistant Editor, and ctrl-drag from the Weather View object over to the top of the StatusMenuController class to create a `weatherView` outlet. While we're there, we're going to add a `weatherMenuItem` class var: 698 | 699 | ```swift 700 | class StatusMenuController: NSObject { 701 | @IBOutlet weak var statusMenu: NSMenu! 702 | @IBOutlet weak var weatherView: WeatherView! 703 | var weatherMenuItem: NSMenuItem! 704 | ... 705 | ``` 706 | 707 | In StatusMenuController's `awakeFromNib` method, before the call to `updateWeather`, add: 708 | 709 | ```swift 710 | weatherMenuItem = statusMenu.item(withTitle: "Weather") 711 | weatherMenuItem.view = weatherView 712 | ``` 713 | 714 | And now `updateWeather` is even simpler: 715 | 716 | ```swift 717 | func updateWeather() { 718 | weatherAPI.fetchWeather("Seattle, WA") { weather in 719 | self.weatherView.update(weather) 720 | } 721 | } 722 | ``` 723 | 724 | Run it! 725 | 726 | ## Adding the Weather Image 727 | 728 | So, we're obviously missing something in our weather view. Let's update it with the appropriate weather image. 729 | 730 | The images for the various weather conditions can be found at http://openweathermap.org/weather-conditions, but I've put them in a [zip file](assets/weather-icons.zip) for you. You can just unzip that and drag the whole folder into _Assets.xcassets_. 731 | 732 | ![Drag Weather Images to Assets](assets/weather-images-assets.png) 733 | 734 | We need to update _WeatherAPI_ to capture the icon code. In the _Weather_ struct in _WeatherAPI.swift_, add: 735 | 736 | ```swift 737 | var icon: String 738 | ``` 739 | 740 | and in `weatherFromJSONData`, add that to the Weather initialization: 741 | 742 | ```swift 743 | let weather = Weather( 744 | city: json["name"] as! String, 745 | currentTemp: mainDict["temp"] as! Float, 746 | conditions: weatherDict["main"] as! String, 747 | icon: weatherDict["icon"] as! String 748 | ) 749 | ``` 750 | 751 | Now in the `update` method of WeatherView, add: 752 | 753 | ```swift 754 | self.imageView.image = NSImage(named: weather.icon) 755 | ``` 756 | 757 | That's it! Run it. Pretty! 758 | 759 | ![Running App with Weather View](assets/app-with-weather-view.png) 760 | 761 | ## Preferences 762 | 763 | Having the city hard-coded in the app is not cool. Let's make a Preferences pane so we can change it. 764 | 765 | Open up _MainMenu.xib_ and drag another MenuItem onto the menu, above Quit, naming it "Preferences...". 766 | 767 | Open up the Assistant Editor again with _StatusMenuController_, and ctrl-drag from the Preferences menu item over to the code and create a "preferencesClicked" action (again with type `NSMenuItem`). 768 | 769 | New ⟶ File ⟶ macOS Source ⟶ Cocoa Class. Call it "PreferencesWindow", set the subclass to NSWindowController, and check the box to create a XIB file. Save it. 770 | 771 | Click on _PreferencesWindow.xib_, and in the Attributes Inspector (⌥⌘4), give the window a title of Preferences. Add a label for "City:", and put a Text Field to the right of it. It should look something like this: 772 | 773 | ![](assets/preferences.png) 774 | 775 | Fancy! 776 | 777 | Bring up the Assistant Editor with _PreferencesWindow.swift_ and ctrl-drag from the text field to the code and create an outlet named "cityTextField". 778 | 779 | In PreferencesWindow.swift, add: 780 | 781 | ```swift 782 | override var windowNibName : String! { 783 | return "PreferencesWindow" 784 | } 785 | ``` 786 | 787 | and at the end of `windowDidLoad()`, add: 788 | 789 | ```swift 790 | self.window?.center() 791 | self.window?.makeKeyAndOrderFront(nil) 792 | NSApp.activate(ignoringOtherApps: true) 793 | ``` 794 | 795 | So _PreferencesWindow.swift_ should look like: 796 | 797 | ```swift 798 | import Cocoa 799 | 800 | class PreferencesWindow: NSWindowController { 801 | @IBOutlet weak var cityTextField: NSTextField! 802 | 803 | override var windowNibName : String! { 804 | return "PreferencesWindow" 805 | } 806 | 807 | override func windowDidLoad() { 808 | super.windowDidLoad() 809 | 810 | self.window?.center() 811 | self.window?.makeKeyAndOrderFront(nil) 812 | NSApp.activate(ignoringOtherApps: true) 813 | } 814 | } 815 | ``` 816 | 817 | In _StatusMenuController.swift_, add a `preferencesWindow` class var: 818 | 819 | ```swift 820 | var preferencesWindow: PreferencesWindow! 821 | ``` 822 | 823 | and initialize in `awakeFromNib()`, before the call to `updateWeather()`: 824 | 825 | ```swift 826 | preferencesWindow = PreferencesWindow() 827 | ``` 828 | 829 | Finally, in the `preferencesClicked` function, add: 830 | 831 | ```swift 832 | preferencesWindow.showWindow(nil) 833 | ``` 834 | 835 | If you run now, selecting the Preferences... menu item should bring up the preferences window. 836 | 837 | Now, let's actually save and update the city. 838 | 839 | Make the PreferencesWindow class an `NSWindowDelegate`: 840 | 841 | ```swift 842 | class PreferencesWindow: NSWindowController, NSWindowDelegate { 843 | ``` 844 | 845 | and add: 846 | 847 | ```swift 848 | func windowWillClose(_ notification: Notification) { 849 | let defaults = UserDefaults.standard 850 | defaults.setValue(cityTextField.stringValue, forKey: "city") 851 | } 852 | ``` 853 | 854 | Now we need to notify the `StatusMenuController` that the preferences have been updated. For this we'll use the delegate pattern. This is easy, but requires a number of edits. First, at the top of PreferencesWindow.swift, add a `PreferencesWindowDelegate` protocol: 855 | 856 | ```swift 857 | protocol PreferencesWindowDelegate { 858 | func preferencesDidUpdate() 859 | } 860 | ``` 861 | 862 | and add a `delegate` instance variable to PreferencesWindow: 863 | 864 | ```swift 865 | var delegate: PreferencesWindowDelegate? 866 | ``` 867 | 868 | Also in PreferencesWindow, at the end of `windowWillClose`, we'll call the delegate: 869 | 870 | ```swift 871 | delegate?.preferencesDidUpdate() 872 | ``` 873 | 874 | Back in StatusMenuController, make it a PreferencesWindowDelegate: 875 | 876 | ```swift 877 | class StatusMenuController: NSObject, PreferencesWindowDelegate { 878 | ``` 879 | 880 | and add the delegate method: 881 | 882 | ```swift 883 | func preferencesDidUpdate() { 884 | updateWeather() 885 | } 886 | ``` 887 | 888 | And in `awakeFromNib`, set the delegate: 889 | 890 | ```swift 891 | preferencesWindow = PreferencesWindow() 892 | preferencesWindow.delegate = self 893 | ``` 894 | 895 | All that's left is to load the city from defaults. First add this at the top of StatusMenuController, **under the imports**: 896 | 897 | ```swift 898 | let DEFAULT_CITY = "Seattle, WA" 899 | ``` 900 | 901 | (...or whatever you want the default to be.) Yes, this is a global variable, and there are probably better ways to do this (like storing it in Info.plist), but that can be left as an exercise for the reader. 902 | 903 | Load the saved city, or default, in `updateWeather`: 904 | 905 | ```swift 906 | func updateWeather() { 907 | let defaults = UserDefaults.standard 908 | let city = defaults.string(forKey: "city") ?? DEFAULT_CITY 909 | weatherAPI.fetchWeather(city) { weather in 910 | self.weatherView.update(weather) 911 | } 912 | } 913 | ``` 914 | 915 | Finally, back in PreferencesWindow.swift, we need to add similar code to load any saved city when we show the preferences. At the end of `windowDidLoad`, add: 916 | 917 | ```swift 918 | let defaults = UserDefaults.standard 919 | let city = defaults.string(forKey: "city") ?? DEFAULT_CITY 920 | cityTextField.stringValue = city 921 | ``` 922 | 923 | Run it! 924 | 925 | ## Next Steps 926 | 927 | That's the end of this tutorial. Obviously there's a lot more that we can do with this, but I'll leave that up to you. Some ideas: 928 | 929 | ### Easy 930 | 931 | - Add other weather info (high/low temp, humidity, sunrise/sunset, etc) to the Weather View 932 | - Change the status menu icon + title to reflect the current conditions 933 | - Add a timer to update the weather regularly (hint: `RunLoop.main.add(refreshTimer!, forMode: RunLoopMode.commonModes)`). 934 | - Make it so clicking on the Weather View opens a browser with detailed weather information (hint: `NSWorkspace.shared().open(url: NSURL)`). Note that since we're using a custom view, the menu item isn't highlighted when you mouse over it, so you'll probably want to do something to make it obvious that it is clickable. 935 | - Add some error handling. Right now if we get an unexpected response from the API, for example, bad things will happen. 936 | - [Write some tests!](https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/testing_with_xcode/Introduction/Introduction.html#//apple_ref/doc/uid/TP40014132-CH1-SW1) 937 | - Add an app icon. This isn't hard, but it can be a pain creating the [various sizes that Apple wants](https://developer.apple.com/library/mac/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/Optimizing/Optimizing.html). Fortunately, there are [tools to make this easier](https://itunes.apple.com/us/app/asset-catalog-creator-app/id809625456?mt=12). 938 | - Add a preference to launch the app on login. I've used the [LaunchAtLoginController](https://github.com/Mozketo/LaunchAtLoginController) library in other apps. There's also [this blog post](http://bdunagan.com/2010/09/25/cocoa-tip-enabling-launch-on-startup/), if you want to try porting the code over to Swift. 939 | - Create an About window. 940 | 941 | ### More Challenging 942 | 943 | - Add support for multiple cities. This will take some effort, especially if the number of cities is dynamic. I think you'll have to put the Weather View in its own XIB, and load it manually (look at `Bundle.main.loadNibNamed(name, owner: owner, options: options)`). The UI in Preferences will obviously need to be updated as well. 944 | 945 | ### You Know Way More Than Me Now 946 | 947 | - Create a completely custom view when clicking on the app in the status bar. See the [Weather Live](https://itunes.apple.com/us/app/weather-live/id755717884?mt=12) app, for example. I haven't tried this, but I suspect it is easier than you might think (depending on how fancy your view is, of course). 948 | 949 | ## Resources 950 | 951 | **[The Swift Programming Language](https://developesur.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/)** - Apple's documentation, also downloadable as a [free iBook](https://itunes.apple.com/us/book/the-swift-programming-language/id881256329?mt=11) 952 | 953 | **[Apple's Swift Blog](https://developer.apple.com/swift/blog/)** and **[Swift Resources](https://developer.apple.com/swift/resources/)** - Straight from the source. 954 | 955 | **[NSHipster](http://nshipster.com/)** - Tons of great, in-depth articles on Objective-C, Swift, and Cocoa 956 | 957 | **[Ray Wenderlich's Tutorials](http://www.raywenderlich.com/tutorials)** - Puts this tutorial to shame. 958 | 959 | **[Mike Ash's NSBlog](https://mikeash.com/pyblog/)** - Great deep dives into Objective-C and Swift 960 | 961 | ## Contact 962 | 963 | Got questions, feedback, or corrections? [Hit me up!](mailto:brad@footle.org) (You can also submit and [issue](https://github.com/bgreenlee/WeatherBar/issues) or [pull request](https://github.com/bgreenlee/WeatherBar/pulls).) 964 | --------------------------------------------------------------------------------