工程结构如下:
- ViewController :捕捉用户手势,发指令给华为盒子。
- WeatherInfo :从网络服务获取空气质量指数和天气预报。
- Settings.bundle :app的一些设置放到iOS的设置里。
重要代码记录如下:
ViewController.h
//
// ViewController.h
// HuaWeiRemoteCtl
//
// Created by jia xiaodong on 12/16/15.
// Copyright (c) 2015 homemade. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <SystemConfiguration/SystemConfiguration.h>
#import "WeatherInfo.h"
enum NetworkStatus
{
NETWORK_NOT_REACHABLE = 0,
NETWORK_THRU_WIFI = 1, // network traffic is through local Wifi
NETWORK_THRU_WWAN = 2, // 3G, GPRS, Edge or other cellular data network
};
@interface ViewController : UIViewController<NSURLConnectionDelegate>
{
// These magic numbers are extracted from http://health.vmall.com/mediaQ/controller.jsp
enum ActionCode
{
ACTION_OK = 0,
ACTION_LEFT = 1,
ACTION_DOWN = 2,
ACTION_RIGHT = 3,
ACTION_UP = 4,
ACTION_BACK = 5,
ACTION_HOME = 6,
ACTION_MENU = 7,
ACTION_POWER = 8,
ACTION_VOL_UP = 9,
ACTION_VOL_DOWN = 10
};
NSString* mBoxIPAddress;
//! user can pan his finger to four directions
enum ActionDirection
{
DIRECTION_INVALID,
DIRECTION_LEFT = ACTION_LEFT,
DIRECTION_DOWN = ACTION_DOWN,
DIRECTION_RIGHT = ACTION_RIGHT,
DIRECTION_UP = ACTION_UP
} mCurrDirection, mPrevDirection;
//! process long press gesture when finger panning
NSTimer* mRepeatDelayer;
NSTimer* mActionRepeater;
//! Single-tap | Double-tap OK
BOOL mIsDoubleTapOK;
UITapGestureRecognizer* mTapGesture;
BOOL mIsShowingForecastInfo;
BOOL mIsShowingTodayDetails;
LocationID mGeoLocation;
//! monitor device's network traffic path
enum NetworkStatus mNetworkStatus;
//! weather info
UILabel* mWeatherInfoBoard;
UIScrollView* mScrollLabel; // text's too long, so need a scroll-effect
UIButton* mToggleInfoButton;
WeatherFullReport* mWeatherInfo;
NSMutableString* mLabelText;
UIActivityIndicatorView* mBusyCircle;
//
BOOL mAqiReady, mDetailReady;
}
@property (nonatomic, copy) NSString* BoxIPAddress;
@property (atomic, assign) enum ActionDirection CurrentDir;
@property (atomic, assign) enum ActionDirection PreviousDir;
- (IBAction)BtnVolDown:(UIButton *)sender;
- (IBAction)BtnVolUp:(UIButton *)sender;
- (IBAction)power:(id)sender;
- (IBAction)home:(id)sender;
- (IBAction)menu:(id)sender;
- (IBAction)back:(id)sender;
//! all remote control's functionalities
- (void)performAction:(enum ActionCode)code;
//! delegate methods
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
//!
- (void)registerTapGesture;
//!
- (void)resetPanTimer;
+ (BOOL)isValidIPv4Address:(NSString*)ip;
//! //! monitor device's network traffic path
- (void) startMonitorNetwork;
- (void) stopMonitorNetwork;
- (void) parseReachabilityFlags: (SCNetworkReachabilityFlags)flags;
//!
- (void) toggleWeatherButton;
@end
ViewController.mm
//
// ViewController.m
// HuaWeiRemoteCtl
//
// Created by jia xiaodong on 12/16/15.
// Copyright (c) 2015 homemade. All rights reserved.
//
#import "ViewController.h"
#include <memory>
#include <netinet/in.h>
NSString* KEY_BOX_IP_ADDRESS = @"box_ip_address";
NSString* KEY_DOUBLE_TAP_OK = @"double_tap_ok";
NSString* DEFAULT_IP_ADDRESS = @"192.168.1.106"; // default box IPv4 address
NSString* KEY_FORECAST_INFO = @"forecast_info";
NSString* KEY_TODAY_DETAILS = @"detailed_forecast";
NSString* KEY_ALARM_INFO = @"alarm_info";
NSString* KEY_LOCATION = @"location_setting";
static SCNetworkReachabilityRef sReachabilityRef;
//! callback which can receive device's event of network path changing
void ReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info)
{
ViewController* checker = static_cast<ViewController *>(info);
[checker parseReachabilityFlags:flags];
}
@interface ViewController ()
@end
@implementation ViewController
@synthesize BoxIPAddress = mBoxIPAddress;
@synthesize CurrentDir = mCurrDirection;
@synthesize PreviousDir = mPrevDirection;
#pragma mark -
#pragma mark variable initialization
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
mBoxIPAddress = DEFAULT_IP_ADDRESS;
mIsDoubleTapOK = YES;
mCurrDirection = mPrevDirection = DIRECTION_INVALID;
mRepeatDelayer = nil;
mActionRepeater = nil;
mTapGesture = nil;
// double tap: button OK
[self registerTapGesture];
// pinch open: volume up; pinch close: volume down
UIPinchGestureRecognizer* pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)];
[self.view addGestureRecognizer:pinch];
[pinch release];
// pan to up, down, left and right direction
UIPanGestureRecognizer* pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
[self.view addGestureRecognizer:pan];
[pan release];
//! show AQI at screen bottom
const int PADDING = 5, HEIGHT = 30;
CGRect rect = [self.view bounds];
mToggleInfoButton = [[UIButton alloc] initWithFrame:CGRectMake(PADDING, rect.size.height-PADDING-HEIGHT, rect.size.width-PADDING*2, HEIGHT)];
[mToggleInfoButton setBackgroundColor:[UIColor grayColor]];
[mToggleInfoButton setTitle:@"查看天气" forState:UIControlStateNormal];
[mToggleInfoButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[mToggleInfoButton setTitleColor:[UIColor darkGrayColor] forState:UIControlStateHighlighted];
[mToggleInfoButton addTarget:self action:@selector(toggleWeatherButton) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:mToggleInfoButton];
mWeatherInfoBoard = nil;
mScrollLabel = nil;
mLabelText = nil;
mBusyCircle = nil;
mWeatherInfo = nil;
[self getCurrentNetworkPath];
[self startMonitorNetwork];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark -
#pragma mark user action feedback
- (IBAction)BtnVolDown:(UIButton *)sender {
[self performAction:ACTION_VOL_DOWN];
}
- (IBAction)BtnVolUp:(UIButton *)sender {
[self performAction:ACTION_VOL_UP];
}
- (IBAction)power:(id)sender {
[self performAction:ACTION_POWER];
}
- (IBAction)home:(id)sender {
[self performAction:ACTION_HOME];
}
- (IBAction)menu:(id)sender {
[self performAction:ACTION_MENU];
}
- (IBAction)back:(id)sender {
[self performAction:ACTION_BACK];
}
- (void)performAction:(enum ActionCode)code {
// all remote control functionalities must work under same local network (WiFi).
if (mNetworkStatus != NETWORK_THRU_WIFI)
{
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
NSString* urlTemplate = [[NSString alloc] initWithFormat:@"http://%@:7766/remote?key=%d", self.BoxIPAddress, code];
NSLog(@"[HuaWei] request: %@", urlTemplate);
NSURL* url = [[NSURL alloc] initWithString:urlTemplate];
NSURLRequest* request = [[NSURLRequest alloc] initWithURL:url
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:1.0];
NSURLConnection* connection = [[NSURLConnection alloc] initWithRequest:request
delegate:self
startImmediately:YES];
[connection release];
[request release];
[url release];
[urlTemplate release];
});
}
- (void)handlePan:(UIPanGestureRecognizer*)gesture {
if (gesture.state == UIGestureRecognizerStateBegan)
{
mCurrDirection = mPrevDirection = DIRECTION_INVALID;
}
else if (gesture.state == UIGestureRecognizerStateChanged)
{
CGPoint pt = [gesture translationInView:self.view];
float xabs = fabsf(pt.x);
float yabs = fabsf(pt.y);
if (xabs > yabs)
{
mCurrDirection = pt.x > 0 ? DIRECTION_RIGHT: DIRECTION_LEFT;
}
else if (xabs < yabs)
{
mCurrDirection = pt.y > 0 ? DIRECTION_DOWN : DIRECTION_UP;
}
if (mPrevDirection != mCurrDirection)
{
//! Firstly, respond to user's input
[self performAction:(enum ActionCode)mCurrDirection];
//! Secondly, begin to monitor user's long press
//! If user keeps initial direction for more than 0.5 second, accelerate that input
[self resetPanTimer];
mRepeatDelayer = [NSTimer scheduledTimerWithTimeInterval:0.5
target:self
selector:@selector(startRepeater)
userInfo:nil
repeats:NO]; // no repeat: run-loop won't keep reference
mPrevDirection = mCurrDirection;
}
}
else if (gesture.state == UIGestureRecognizerStateEnded)
{
mCurrDirection = mPrevDirection = DIRECTION_INVALID;
[self resetPanTimer];
}
}
- (void)startRepeater
{
mRepeatDelayer = nil; // no repeat: run-loop won't keep reference. so no need to invalidate it
mActionRepeater = [NSTimer scheduledTimerWithTimeInterval:0.2
target:self
selector:@selector(handleLongPress:)
userInfo:nil
repeats:YES];
}
- (void)handleTap:(UITapGestureRecognizer*)gesture {
CGPoint pt = [gesture locationInView:self.view];
if (pt.y > 170) // upper screen is full of buttons
{
[self performAction:ACTION_OK];
}
}
- (void)handlePinch:(UIPinchGestureRecognizer*)gesture {
if (gesture.state == UIGestureRecognizerStateEnded)
{
[self performAction:(gesture.scale > 1.0f ? ACTION_VOL_UP : ACTION_VOL_DOWN)];
}
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
/*
NSString* text = [[NSString alloc] initWithFormat:@"[%@] %@", mBoxIPAddress, [error localizedDescription]];
UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"Hua Wei Box"
message:text
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil, nil];
[alert show];
[alert release];
[text release];
*/
}
- (void)handleLongPress:(NSTimer*) timer {
enum ActionCode action = (enum ActionCode)self.CurrentDir;
[self performAction:action];
}
- (void)resetPanTimer {
[mRepeatDelayer invalidate]; // Run-loop will release timer's reference
mRepeatDelayer = nil;
[mActionRepeater invalidate];
mActionRepeater = nil;
}
- (void)registerTapGesture {
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
//! [1] Box IP address
NSUserDefaults *config = [NSUserDefaults standardUserDefaults];
NSString* ip = [config stringForKey:KEY_BOX_IP_ADDRESS];
if ([ViewController isValidIPv4Address:ip] && [ip compare:mBoxIPAddress] != NSOrderedSame)
{
mBoxIPAddress = ip;
}
//! [2] Is double-tap / single-tap effective
BOOL isDoubleTapOK = [config boolForKey:KEY_DOUBLE_TAP_OK];
if (isDoubleTapOK != mIsDoubleTapOK || mTapGesture == nil)
{
mIsDoubleTapOK = isDoubleTapOK;
[self.view removeGestureRecognizer:mTapGesture];
[mTapGesture release];
mTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
[mTapGesture setNumberOfTapsRequired:(mIsDoubleTapOK ? 2 : 1)];
[self.view addGestureRecognizer:mTapGesture];
}
mIsShowingForecastInfo = [config boolForKey:KEY_FORECAST_INFO];
mIsShowingTodayDetails = [config boolForKey:KEY_TODAY_DETAILS];
mGeoLocation = static_cast<LocationID>([config integerForKey:KEY_LOCATION]);
[pool release];
}
+ (BOOL)isValidIPv4Address:(NSString*)ip {
NSString * regex = @"^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
"([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
"([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
"([01]?\\d\\d?|2[0-4]\\d|25[0-5])$";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regex];
return [predicate evaluateWithObject:ip];
}
#pragma mark -
#pragma mark network status detection
- (void) startMonitorNetwork {
if (sReachabilityRef)
{
SCNetworkReachabilityContext context = {0, self, NULL, NULL, NULL};
if (SCNetworkReachabilitySetCallback(sReachabilityRef, ReachabilityCallback, &context))
{
SCNetworkReachabilityScheduleWithRunLoop(sReachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
}
}
}
- (void) getCurrentNetworkPath {
struct sockaddr_in zeroAddr;
bzero(&zeroAddr, sizeof(zeroAddr));
zeroAddr.sin_len = sizeof(zeroAddr);
zeroAddr.sin_family = AF_INET;
sReachabilityRef = SCNetworkReachabilityCreateWithAddress(NULL, (struct sockaddr *) &zeroAddr);
SCNetworkReachabilityFlags flags;
SCNetworkReachabilityGetFlags(sReachabilityRef, &flags);
[self parseReachabilityFlags:flags];
}
- (void) stopMonitorNetwork {
if (sReachabilityRef)
{
SCNetworkReachabilityUnscheduleFromRunLoop(sReachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
}
}
- (void) parseReachabilityFlags: (SCNetworkReachabilityFlags)flags
{
if (flags & kSCNetworkFlagsReachable)
{
mNetworkStatus = (flags & kSCNetworkReachabilityFlagsIsWWAN) ? NETWORK_THRU_WWAN : NETWORK_THRU_WIFI;
}
else
{
mNetworkStatus = NETWORK_NOT_REACHABLE;
}
}
#pragma mark -
#pragma mark UI of weather info
- (void) toggleWeatherButton
{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
std::unique_ptr<NSAutoreleasePool, void(*)(NSAutoreleasePool*)> scopePool(pool, [](NSAutoreleasePool* p) {
[p release];
});
if (mWeatherInfo == nil)
{
if (mNetworkStatus == NETWORK_NOT_REACHABLE)
{
UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"天气"
message:@"没网,请先联网!"
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alert show];
[alert release];
return;
}
mLabelText = [[NSMutableString alloc] init];
[self startActivityAnimation];
[mToggleInfoButton setTitle:@"关闭" forState:UIControlStateNormal];
mWeatherInfo = [[WeatherFullReport alloc] initWithLocation:mGeoLocation];
[self checkWeatherSetting];
[mWeatherInfo queryWithCompletionHandler:^(BOOL aqiReady, BOOL otherReady) {
if (aqiReady) {
mAqiReady = TRUE;
}
if (otherReady) {
mDetailReady = TRUE;
}
[mLabelText setString:@""];
[self organizeTextForPresentation];
dispatch_async(dispatch_get_main_queue(), ^{
if (mScrollLabel != nil) {
[self destroyScrollLabel];
}
[self createScrollLable];
[mWeatherInfoBoard setText:mLabelText];
if (mAqiReady && mDetailReady) {
[self stopActivityAnimation];
mAqiReady = mDetailReady = FALSE; // reset
}
});
}];
}
else
{
[self destroyScrollLabel];
[mWeatherInfo release]; mWeatherInfo = nil;
[mLabelText release]; mLabelText = nil;
[self stopActivityAnimation];
[mToggleInfoButton setTitle:@"查看天气" forState:UIControlStateNormal];
}
}
- (void)checkWeatherSetting
{
mWeatherInfo.details.options = WEATHER_NOW;
if (mIsShowingForecastInfo) {
mWeatherInfo.details.options |= WEATHER_FORECAST;
}
if (mIsShowingTodayDetails) {
mWeatherInfo.details.options |= WEATHER_DETAIL_FORECAST;
}
}
- (void) createScrollLable
{
CGRect rect = [self.view bounds];
CGFloat PADDING = 5;
CGFloat X = PADDING, Y = PADDING + 170;
CGFloat SCROLL_WIDTH = rect.size.width-2*PADDING;
CGFloat SCROLL_HEIGHT = rect.size.height-Y-50;
mScrollLabel = [[UIScrollView alloc] initWithFrame:CGRectMake(X, Y, SCROLL_WIDTH, SCROLL_HEIGHT)];
UIFont* font = [UIFont systemFontOfSize:14];
CGSize contentSize = [mLabelText sizeWithFont:font constrainedToSize:CGSizeMake(SCROLL_WIDTH, 2048)];
mWeatherInfoBoard = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, contentSize.width, contentSize.height)];
mWeatherInfoBoard.font = font;
mWeatherInfoBoard.lineBreakMode = NSLineBreakByWordWrapping;
mWeatherInfoBoard.numberOfLines = 0;
mWeatherInfoBoard.backgroundColor = [UIColor clearColor];
mScrollLabel.contentSize = mWeatherInfoBoard.frame.size;
[mScrollLabel addSubview:mWeatherInfoBoard];
[self.view addSubview:mScrollLabel];
}
- (void) destroyScrollLabel
{
[mScrollLabel removeFromSuperview];
[mWeatherInfoBoard removeFromSuperview];
[mWeatherInfoBoard release]; mWeatherInfoBoard = nil;
[mScrollLabel release]; mScrollLabel = nil;
}
- (void) organizeTextForPresentation
{
if ([@"" compare:mWeatherInfo.aqi.result] != NSOrderedSame) {
[mLabelText appendFormat:@"%@", mWeatherInfo.aqi.result];
}
if ([@"" compare:mWeatherInfo.details.result] != NSOrderedSame) {
[mLabelText appendFormat:@"\n%@", mWeatherInfo.details.result];
}
}
- (void) startActivityAnimation
{
CGRect screen = self.view.bounds;
mBusyCircle = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(screen.size.width/2, screen.size.height/2, 10, 10)];
[mBusyCircle setColor:[UIColor grayColor]];
[self.view addSubview:mBusyCircle];
[mBusyCircle startAnimating];
}
- (void) stopActivityAnimation
{
if (mBusyCircle) {
[mBusyCircle stopAnimating];
[mBusyCircle removeFromSuperview];
[mBusyCircle release]; mBusyCircle = nil;
}
}
@end
WeatherInfo.h
//
// WeatherInfo.h
// HuaWeiRemoteCtl
//
// Created by jia xiaodong on 5/2/16.
// Modified on 2/7/17
//
typedef NS_ENUM(NSUInteger, LocationID)
{
Beijing_GuoFengMeiLun,
Beijing_ZhongGuanCun,
ShiJiaZhuang_WorkerHospital,
QinHuangDao_LuLong
};
#pragma mark -
#pragma mark a common interface for general purpose Weather info provider
@protocol WeatherServiceProvider <NSObject>
@required
- (void)launchQuery:(LocationID)location completionHander:(void(^)(BOOL success))handler;
+ (NSString*)StringFromId:(LocationID)loc;
@end
#pragma mark -
#pragma mark Air Quality Index from aqicn.org
/** http://aqicn.org/map/beijing/cn/
A professional AQI website, free of charge, gobally covered.
*/
@interface AirQualityIndex : NSObject <WeatherServiceProvider>
{
NSString* mPM2_5;
}
@property (readonly) NSString* result;
- (void)launchQuery:(LocationID)location completionHander:(void(^)(BOOL success))handler;
+ (NSString*)StringFromId:(LocationID)loc;
@end
#pragma mark -
#pragma mark a base class for detailed weather info
typedef NS_OPTIONS(NSUInteger, WeatherOption)
{
WEATHER_NOW = 1<<0, // today's weather
WEATHER_FORECAST = 1<<1, // weather forecast for several days of future
WEATHER_DETAIL_FORECAST = 1<<2, // detailed forecast (for today only, hourly forecast)
WEATHER_ALARM = 1<<3 // not implemented
};
@interface DetailedWeatherInfo : NSObject <WeatherServiceProvider>
{
WeatherOption mWeatherOptions;
NSString* mResult;
}
@property WeatherOption options;
@property (nonatomic, copy) NSString* result;
@end
#pragma mark -
#pragma mark weather service from www.heweather.com (free for personal use)
/**
Registered as a free user by an email address, you can get 3000 queries per day.
Personal key: http://console.heweather.com/my/service
Docs: http://docs.heweather.com/224291
City list: http://docs.heweather.com/224293
*/
@interface HeWeatherInfoNode : NSObject
{
NSString* date;
NSString* astro; // rise/set time of sun and moon
NSString* condition; // sunny, cloudy, rainny, ...
NSString* temperature; // Celsius degree
NSString* humidity; // relative humidity (%)
NSString* probability; // probability of precipitation
NSString* precipitation; // amount of precipitation (mm)
NSString* pressure; // atmospheric pressure (mmHg)
NSString* uv; // ultraviolet-ray radiation degree
NSString* visibility; // km
NSString* wind; // wind
}
@property (nonatomic, copy) NSString *date, *astro, *condition, *probability;
@property (nonatomic, copy) NSString *temperature, *humidity, *precipitation;
@property (nonatomic, copy) NSString *pressure, *uv, *visibility, *wind;
@property (nonatomic, readonly) NSString *description;
- (id)initWithJson:(NSDictionary *)nfo;
@end
@interface HeWeather : DetailedWeatherInfo
{
NSString* mAqi; // air quality index
HeWeatherInfoNode* mNow;
NSArray<HeWeatherInfoNode *> *mForecast;
NSArray<HeWeatherInfoNode *> *mDetailedForecast;
}
@end
#pragma mark -
#pragma mark put all weather info together to make a report
@interface WeatherFullReport : NSObject
{
LocationID mLocation;
AirQualityIndex *mAQI;
DetailedWeatherInfo *mWeatherProvider;
}
@property (nonatomic) LocationID location;
@property (nonatomic, readonly) AirQualityIndex *aqi;
@property (nonatomic, readonly) DetailedWeatherInfo *details;
- (id)initWithLocation:(LocationID)location;
- (void)queryWithCompletionHandler:(void (^)(BOOL aqiReady, BOOL otherReady))handler;
@end
WeatherInfo.mm
//
// WeatherInfo.mm
// HuaWeiRemoteCtl
//
// Created by jia xiaodong on 5/2/16.
// Modified on 2/7/17
//
#import <Foundation/Foundation.h>
#import "WeatherInfo.h"
#include <memory>
#pragma mark -
#pragma mark AirQualityIndex
@implementation AirQualityIndex
@synthesize result = mPM2_5;
- (id)init
{
if (self = [super init])
{
mPM2_5 = nil;
}
return self;
}
/* After analyzing aqicn.org by Web Developer Tools in Firefox, I got below 2 APIs:
https://api.waqi.info/api/feed/@{city_id}/obs.en.json : super detailed info
https://api.waqi.info/api/feed/@{city_id}/now.json : concise info
the 2nd is fairly enough to fit my needs. I only need an AQI value, not its components.
*/
- (void)launchQuery:(LocationID)loc completionHander:(void(^)(BOOL success))handler
{
NSString* location = [AirQualityIndex StringFromId:loc];
NSString* urlTemplate = [[NSString alloc] initWithFormat:@"https://api.waqi.info/api/feed/@%@/now.json", location];
NSURL* url = [[NSURL alloc] initWithString:urlTemplate];
NSURLSessionDataTask* webRequest = [[NSURLSession sharedSession] dataTaskWithURL:url
completionHandler:^(NSData* data, NSURLResponse* response, NSError* error)
{
if (!error && ([(NSHTTPURLResponse*)response statusCode] == 200))
{
[self parseResponse:data];
// callback
if (handler)
{
handler(mPM2_5 != nil);
}
}
}];
[webRequest resume];
[url release];
[urlTemplate release];
}
//! the location code is found through Web Developer Tools in Firefox
//! when opening aqicn.org webpage.
+ (NSString*)StringFromId:(LocationID)loc
{
switch (loc) {
case Beijing_ZhongGuanCun: // Wanliu, Haidian
return @"452";
case Beijing_GuoFengMeiLun:// BDA, Yizhuang
return @"460";
case ShiJiaZhuang_WorkerHospital:
return @"644";
case QinHuangDao_LuLong:
return @"5614";
default:
return @"";
}
}
- (void)parseResponse:(NSData*)response
{
std::unique_ptr<NSAutoreleasePool, void(*)(NSAutoreleasePool*)> scopePool(
[[NSAutoreleasePool alloc] init], // pool ptr
[](NSAutoreleasePool* p) { [p release]; }); // deleter
NSError* err = nil;
NSDictionary* info = [NSJSONSerialization JSONObjectWithData:response
options:NSJSONReadingMutableLeaves
error:&err];
NSDictionary* dict = [info objectForKey:@"rxs"];
if (!dict) {
return;
}
if ([@"1" compare:[dict objectForKey:@"ver"]] != NSOrderedSame) {
return;
}
if ([@"ok" compare:[dict objectForKey:@"status"]] != NSOrderedSame) {
return;
}
dict = [[dict objectForKey:@"obs"] objectAtIndex:0];
if ([@"ok" compare:[dict objectForKey:@"status"]] != NSOrderedSame) {
return;
}
dict = [dict objectForKey:@"msg"];
NSNumber* aqi = [dict objectForKey:@"aqi"];
NSDictionary* city = [dict objectForKey:@"city"];
dict = [dict objectForKey:@"time"];
NSString* time = [NSString stringWithFormat:@"%@, %@",[dict objectForKey:@"s"], [dict objectForKey:@"tz"]];
mPM2_5 = [[NSString alloc] initWithFormat:@"%@\n%@\nAQI (from aqicn.org): %lu\n", time, [city objectForKey:@"name"], [aqi unsignedLongValue]];
}
@end
#pragma mark -
#pragma mark DetailedWeatherInfo: not a discrete class
@implementation DetailedWeatherInfo
@synthesize options = mWeatherOptions;
@synthesize result;
- (id)init
{
if (self = [super init]) {
mWeatherOptions = WEATHER_NOW;
mResult = nil;
}
return self;
}
- (void)dealloc
{
[mResult release];
[super dealloc];
}
@end
#pragma mark -
#pragma mark HeWeather: a so called "free forever" service
@implementation HeWeatherInfoNode
@synthesize date, astro, condition, temperature, humidity;
@synthesize precipitation, pressure, uv, visibility, wind;
@synthesize probability;
@dynamic description;
- (id)init
{
if (self = [super init]) {
self.date = nil;
self.astro = nil;
self.condition = nil;
self.temperature = nil;
self.humidity = nil;
self.probability = nil;
self.precipitation = nil;
self.pressure = nil;
self.uv = nil;
self.visibility = nil;
self.wind = nil;
}
return self;
}
- (id)initWithJson:(NSDictionary *)nfo
{
self = [self init];
if (self) {
NSString* value = nil;
if ((value = [nfo objectForKey:@"date"])) {
self.date = [value copy];
} else {
NSDate* now = [NSDate date];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy-MM-dd HH:mm"];
self.date = [[formatter stringFromDate:now] copy];
[formatter release];
}
NSDictionary *dict = [nfo objectForKey:@"astro"];
if (dict) {
NSString *sunrise = [dict objectForKey:@"sr"];
NSString *sunset = [dict objectForKey:@"ss"];
NSString *moonrise = [dict objectForKey:@"mr"];
NSString *moonset = [dict objectForKey:@"ms"];
self.astro = [[NSString alloc] initWithFormat:@"日升落: %@ - %@; 月升落: %@ - %@", sunrise, sunset, moonrise, moonset];
}
if ((dict = [nfo objectForKey:@"cond"])) {
if ((value = [dict objectForKey:@"txt"])) {
self.condition = [value copy];
} else {
self.condition = [[NSString alloc] initWithFormat:@"日%@,夜%@", [dict objectForKey:@"txt_d"], [dict objectForKey:@"txt_n"]];
}
}
if ((value = [nfo objectForKey:@"tmp"])) {
if ([value isKindOfClass:[NSDictionary class]]) {
dict = [nfo objectForKey:@"tmp"];
value = [NSString stringWithFormat:@"[%@,%@]", [dict objectForKey:@"min"], [dict objectForKey:@"max"]];
}
NSMutableString* temp = [NSMutableString stringWithFormat:@"温度: %@", value];
if ((value = [nfo objectForKey:@"fl"])) {
[temp appendFormat:@"; 体感温度: %@", value];
}
self.temperature = [temp copy];
}
if ((value = [nfo objectForKey:@"hum"])) {
self.humidity = [[NSString alloc] initWithFormat:@"相对湿度: %@%%", value];
}
if ((value = [nfo objectForKey:@"pop"])) {
float p = [value floatValue];
if (p > 0) {
self.probability = [[NSString alloc] initWithFormat:@"降水概率: %d%%", NSUInteger(p*100)];
}
}
if ((value = [nfo objectForKey:@"pcpn"])) {
if ([value floatValue] > 0) {
self.precipitation = [[NSString alloc] initWithFormat:@"降水量: %@mm", value];
}
}
if ((value = [nfo objectForKey:@"pres"])) {
self.pressure = [[NSString alloc] initWithFormat:@"大气压: %@mmHg", value];
}
if ((value = [nfo objectForKey:@"uv"])) {
self.uv = [[NSString alloc] initWithFormat:@"紫外线: %@", value];
}
if ((value = [nfo objectForKey:@"vis"])) {
self.visibility = [[NSString alloc] initWithFormat:@"能见度: %@km", value];
}
if ((dict = [nfo objectForKey:@"wind"])) {
NSString *dir = [dict objectForKey:@"dir"];
NSString *lvl = [dict objectForKey:@"sc"];
NSString *spd = [dict objectForKey:@"spd"];
self.wind = [[NSString alloc] initWithFormat:@"%@%@级, %@km/h", dir, lvl, spd];
}
}
return self;
}
- (void)dealloc
{
[date release];
[astro release];
[condition release];
[temperature release];
[humidity release];
[probability release];
[precipitation release];
[pressure release];
[uv release];
[visibility release];
[wind release];
[super dealloc];
}
- (NSString *)description
{
NSMutableString *desc = [NSMutableString stringWithFormat:@"------- %@ -------", date];
if (condition) {
[desc appendFormat:@"\n%@", condition];
}
if (astro) {
[desc appendFormat:@"\n%@", astro];
}
if (temperature) {
[desc appendFormat:@"\n%@", temperature];
}
if (humidity) {
[desc appendFormat:@"\n%@", humidity];
}
if (probability) {
[desc appendFormat:@"\n%@", probability];
}
if (precipitation) {
[desc appendFormat:@"\n%@", precipitation];
}
if (pressure) {
[desc appendFormat:@"\n%@", pressure];
}
if (uv) {
[desc appendFormat:@"\n%@", uv];
}
if (visibility) {
[desc appendFormat:@"\n%@", visibility];
}
if (wind) {
[desc appendFormat:@"\n%@", wind];
}
return [desc copy];
}
@end
@implementation HeWeather
@dynamic result;
- (id)init
{
if (self = [super init]) {
mAqi = nil;
mNow = nil;
mForecast = nil;
mDetailedForecast = nil;
}
return self;
}
- (void)dealloc
{
[mAqi release];
[mNow release];
//[mForecast enumerateObjectsUsingBlock:^(HeWeatherInfoNode *obj, NSUInteger idx, BOOL *stop) {
// [obj release];
//}];
[mForecast release];
//[mDetailedForecast enumerateObjectsUsingBlock:^(HeWeatherInfoNode *obj, NSUInteger idx, BOOL *stop) {
// [obj release];
//}];
[mDetailedForecast release];
[super dealloc];
}
+ (NSString*)StringFromId:(LocationID)loc
{
switch (loc) {
case Beijing_GuoFengMeiLun: // Tongzhou, Beijing
return @"CN101010600";
case Beijing_ZhongGuanCun:
return @"CN101010200"; // Haidian, Beijing
case ShiJiaZhuang_WorkerHospital:
return @"CN101090101"; // Shijiazhuang City
case QinHuangDao_LuLong:
return @"CN101091105"; // Lulong County, Qinhuangdao City
default:
return @"CN101010100"; // beijing
}
}
- (void)launchQuery:(LocationID)loc completionHander:(void(^)(BOOL success))handler
{
NSString* serviceTemplate = @"https://free-api.heweather.com/v5/[api]?key=2dae4ca04d074a1abde0c113c3292ae1&city=";
serviceTemplate = [serviceTemplate stringByReplacingOccurrencesOfString:@"[api]" withString:@"weather"];
serviceTemplate = [serviceTemplate stringByAppendingString:[HeWeather StringFromId:loc]];
NSURL* url = [NSURL URLWithString:serviceTemplate];
NSURLSessionDataTask* webRequest = [[NSURLSession sharedSession] dataTaskWithURL:url
completionHandler:^(NSData* data, NSURLResponse* response, NSError* error)
{
BOOL success = (!error && ([(NSHTTPURLResponse*)response statusCode] == 200));
if (success)
{
[self parseResponse:data];
}
if (handler) // callback
{
handler(success);
}
}];
[webRequest resume];
}
- (void)parseResponse:(NSData*)response
{
std::unique_ptr<NSAutoreleasePool, void(*)(NSAutoreleasePool*)> scopePool(
[[NSAutoreleasePool alloc] init], // pool ptr
[](NSAutoreleasePool* p) { [p release]; }); // deleter
NSError* err = nil;
NSDictionary* info = [NSJSONSerialization JSONObjectWithData:response
options:NSJSONReadingMutableLeaves
error:&err];
NSArray* array = [info objectForKey:@"HeWeather5"];
if (!array || [array count] == 0) {
return;
}
NSDictionary* node = [array objectAtIndex:0]; // only 1 node available and meaningful
if ([@"ok" compare:[node objectForKey:@"status"]] != NSOrderedSame) {
return;
}
[self parseAqi:[[node objectForKey:@"aqi"] objectForKey:@"city"]];
[self parseNowInfo:[node objectForKey:@"now"]];
if (self.options & WEATHER_FORECAST)
{
[self parseDailyForecast:[node objectForKey:@"daily_forecast"]];
}
if (self.options & WEATHER_DETAIL_FORECAST)
{
[self parseDetailedForecast:[node objectForKey:@"hourly_forecast"]];
}
}
- (void)parseAqi:(NSDictionary *)info
{
NSString* aqi = [info objectForKey:@"aqi"];
NSString* p10 = [info objectForKey:@"pm10"];
NSString* p25 = [info objectForKey:@"pm25"];
NSString* qty = [info objectForKey:@"qlty"];
mAqi = [[NSString alloc] initWithFormat:@"AQI:%@, PM10:%@, PM2.5:%@, %@", aqi, p10, p25, qty];
}
- (void)parseNowInfo:(NSDictionary *)now
{
mNow = [[HeWeatherInfoNode alloc] initWithJson:now];
}
- (void)parseDailyForecast:(NSArray *)forecast
{
NSMutableArray<HeWeatherInfoNode *> *array = [[NSMutableArray alloc] init];
[forecast enumerateObjectsUsingBlock:^(NSDictionary *obj, NSUInteger idx, BOOL *stop) {
HeWeatherInfoNode *node = [[HeWeatherInfoNode alloc] initWithJson:obj];
[array insertObject:node atIndex:idx];
[node release];
}];
mForecast = [[NSArray alloc] initWithArray:array];
[array release];
}
- (void)parseDetailedForecast:(NSArray *)forecast
{
NSMutableArray<HeWeatherInfoNode *> *array = [[NSMutableArray alloc] init];
[forecast enumerateObjectsUsingBlock:^(NSDictionary *obj, NSUInteger idx, BOOL *stop) {
HeWeatherInfoNode *node = [[HeWeatherInfoNode alloc] initWithJson:obj];
[array insertObject:node atIndex:idx];
[node release];
}];
mDetailedForecast = [[NSArray alloc] initWithArray:array];
[array release];
}
- (NSString *)result
{
if (mNow) {
NSMutableString* tmp = [[NSMutableString alloc] initWithString:mNow.description];
if (self->mAqi) {
[tmp appendFormat:@"\n%@\n", self->mAqi];
}
[mForecast enumerateObjectsUsingBlock:^(HeWeatherInfoNode *obj, NSUInteger idx, BOOL *stop) {
[tmp appendFormat:@"\n%@\n", obj.description];
}];
[mDetailedForecast enumerateObjectsUsingBlock:^(HeWeatherInfoNode *obj, NSUInteger idx, BOOL *stop) {
[tmp appendFormat:@"\n%@", obj.description];
}];
mResult = [tmp copy];
[tmp release];
}
return mResult;
}
@end
#pragma mark -
#pragma mark WeatherFullReport
@implementation WeatherFullReport
@synthesize aqi = mAQI;
@synthesize details = mWeatherProvider;
@synthesize location = mLocation;
- (id)initWithLocation:(LocationID)loc
{
if (self = [super init]) {
mLocation = loc;
mAQI = [[AirQualityIndex alloc] init];
mWeatherProvider = [[HeWeather alloc] init];
}
return self;
}
- (void) queryWithCompletionHandler:(void (^)(BOOL aqiReady, BOOL otherReady))handler;
{
[mAQI launchQuery:mLocation completionHander:^(BOOL success){
if (handler)
{
handler(YES, NO);
}
}];
[mWeatherProvider launchQuery:mLocation completionHander:^(BOOL success){
if (handler)
{
handler(NO, YES);
}
}];
}
- (void) dealloc
{
[mAQI release];
[mWeatherProvider release];
[super dealloc];
}
@end