iOS app: Weather Forecast and Huawei Remote Control

工程结构如下:

  1. ViewController :捕捉用户手势,发指令给华为盒子。
  2. WeatherInfo :从网络服务获取空气质量指数和天气预报。
  3. 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


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值