在iOS中,最常見的網絡請求方式是NSURLSession,它是蘋果推薦的現代API,簡單安全且易于拓展。
一次完整的網絡請求流程:
構造?NSURL?對象
創建?NSURLSessionDataTask
發起請求(resume)
在回調中解析數據
回到主線程更新 UI
下面,我們用一個簡單的程序來試一下。
NSString *city = @"Beijing";
NSString *apiKey = @"你的API密鑰";
NSString *urlString = [NSString stringWithFormat:@"https://api.weatherapi.com/v1/current.json?key=%@&q=%@&lang=zh", apiKey, city];
urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];NSURL *url = [NSURL URLWithString:urlString];
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {if (error) {NSLog(@"請求失敗:%@", error.localizedDescription);return;}NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];NSLog(@"天氣數據:%@", json);dispatch_async(dispatch_get_main_queue(), ^{// 更新 UI});
}];
[task resume];
首頁
[[NSNotificationCenter defaultCenter] addObserver:selfselector:@selector(handleAddCityNotification:)name:@"AddNewCityNotification"object:nil];[[NSNotificationCenter defaultCenter] addObserver:selfselector:@selector(handleDeleteCityNotification:)name:@"DeleteCityNotification"object:nil];
接收兩個通知,一個是搜索頁面的城市,另一個是詳情頁面的刪除指令。
?
-(void) handleAddCityNotification:(NSNotification *)notification {NSDictionary *userInfo = [notification userInfo];NSString *newCity = userInfo[@"cityName"];BOOL exists = NO;for (NSDictionary *city in self.cityData) {if ([city[@"name"] isEqualToString:newCity]) {exists = YES;break;}}if (!exists) {[self.cityData addObject:@{@"name": newCity}];[self createUrl];}
}- (void)handleDeleteCityNotification:(NSNotification *)notification {NSDictionary *userInfo = [notification userInfo];NSString *cityName = userInfo[@"cityName"];// 從城市列表中刪除該城市NSMutableArray *citiesToRemove = [NSMutableArray array];for (NSDictionary *city in self.cityData) {if ([city[@"name"] isEqualToString:cityName]) {[citiesToRemove addObject:city];}}[self.cityData removeObjectsInArray:citiesToRemove];[self.tableView reloadData];
}
這個頁面要執行兩個操作: 點擊加號添加城市,刪除城市。
這里給出我的網絡請求代碼:?
- (void)createUrl {if (self.cityData.count == 0) return;dispatch_group_t group = dispatch_group_create();NSURLSession *session = [NSURLSession sharedSession];NSMutableArray *tempDicArray = [NSMutableArray array];NSLog(@"1");for (NSDictionary *city in self.cityData) {dispatch_group_enter(group);NSString *cityName = city[@"name"];NSString *urlString = [NSString stringWithFormat:@"https://api.weatherapi.com/v1/current.json?key=2db0265c1d084416b9275428252207&q=%@&lang=zh ", cityName];urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];NSURL *url = [NSURL URLWithString:urlString];NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {if (error) {NSLog(@"錯錯錯!! %@: %@", city, error.localizedDescription);dispatch_group_leave(group);return;}if (data) {NSError *jsonError;//解析數據NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];if (jsonError) {NSLog(@"JSON解析失敗: %@", jsonError.localizedDescription);} else {NSLog(@"API響應: %@", dic);// 檢查是否有錯誤if (dic[@"error"]) {NSLog(@"API返回錯誤: %@", dic[@"error"][@"message"]);} else {@synchronized (tempDicArray) {//賦值到tempDicArray[tempDicArray addObject:dic];NSLog(@"成功添加城市: %@", cityName);}}}} else {NSLog(@"無響應數據");}dispatch_group_leave(group);}];[task resume];
}dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@"所有請求完成,獲取到%ld個城市數據", tempDicArray.count);//最終到dicArrayself.dicArray = tempDicArray;[self.tableView reloadData];});
}
// 添加支持滑動刪除
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {return YES;
}
搜索頁面
-(void) addCityToMain:(NSDictionary *)cityInfo {NSString *cityName = cityInfo[@"name"];NSDictionary *userInfo = @{@"cityName": cityName};[[NSNotificationCenter defaultCenter] postNotificationName:@"AddNewCityNotification" object:nil userInfo:userInfo];// 關閉添加頁面
// [self dismissViewControllerAnimated:YES completion:nil];
}
//選中哪個就傳給主頁
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {[tableView deselectRowAtIndexPath:indexPath animated:YES];if (indexPath.row < self.searchResults.count) {NSDictionary *city = self.searchResults[indexPath.row];DetailViewController *detailVC = [[DetailViewController alloc] init];detailVC.cityName = city[@"name"];detailVC.canAddCity = YES;UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:detailVC];nav.modalPresentationStyle = UIModalPresentationFullScreen;[self presentViewController:nav animated:YES completion:nil];}
}
當我們點擊其中一行時,我們會跳轉到該城市
城市詳細天氣
給出我的每小時天氣預報代碼
- (void)setupHourlyForecastData {NSArray *forecastDays = self.weatherData[@"forecast"][@"forecastday"];if (forecastDays.count == 0) return;//WeatherAPI 返回的 forecast 結構中,每天包含一個 hour 數組(24 小時)。為了支持“跨天展示”,你先拼接所有天的小時數組// 拼接所有 forecastday 的 hour 數據NSMutableArray *allHours = [NSMutableArray array];for (NSDictionary *day in forecastDays) {NSArray *hours = day[@"hour"];if (hours) [allHours addObjectsFromArray:hours];}if (allHours.count == 0) return;UIView *hourlyCard = [self.contentView viewWithTag:1000];UIScrollView *scrollView = [hourlyCard viewWithTag:1001];if (!scrollView) return;CGFloat itemWidth = 60;CGFloat spacing = 10;//當前小時NSDate *now = [NSDate date];NSDateFormatter *formatter = [[NSDateFormatter alloc] init];formatter.dateFormat = @"yyyy-MM-dd HH";NSString *nowHourString = [formatter stringFromDate:now];NSInteger startIndex = 0;//找出當前時間匹配的小時for (int i = 0; i < allHours.count; i++) {NSString *hourString = [allHours[i][@"time"] substringToIndex:13];if ([hourString isEqualToString:nowHourString]) {startIndex = i;break;}}//主要是防止數組越界NSInteger count = MIN(24, allHours.count - startIndex);//給7個格子賦值for (int i = 0; i < count; i++) {NSDictionary *hour = allHours[startIndex + i];NSString *time = [hour[@"time"] substringFromIndex:11];NSString *temp = [NSString stringWithFormat:@"%.0f°", [hour[@"temp_c"] floatValue]];NSString *iconUrl = [NSString stringWithFormat:@"https:%@", hour[@"condition"][@"icon"]];UIView *hourView = [[UIView alloc] initWithFrame:CGRectMake(i * (itemWidth + spacing), 0, itemWidth, 70)];UILabel *timeLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, itemWidth, 20)];timeLabel.text = time;timeLabel.font = [UIFont systemFontOfSize:12];timeLabel.textColor = [UIColor whiteColor];timeLabel.textAlignment = NSTextAlignmentCenter;[hourView addSubview:timeLabel];UIImageView *icon = [[UIImageView alloc] initWithFrame:CGRectMake(10, 20, 40, 30)];icon.contentMode = UIViewContentModeScaleAspectFit;[hourView addSubview:icon];[self loadIcon:iconUrl forImageView:icon];UILabel *tempLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 50, itemWidth, 20)];tempLabel.text = temp;tempLabel.font = [UIFont systemFontOfSize:12];tempLabel.textColor = [UIColor whiteColor];tempLabel.textAlignment = NSTextAlignmentCenter;[hourView addSubview:tempLabel];[scrollView addSubview:hourView];}scrollView.contentSize = CGSizeMake((itemWidth + spacing) * count, 70);
}//每小時天氣
同時,我下面的每部分都以卡片形式呈現
//卡片
- (UIView *)createCardViewWithFrame:(CGRect)frame {UIView *card = [[UIView alloc] initWithFrame:frame];card.backgroundColor = [UIColor colorWithWhite:1 alpha:0.2];card.layer.cornerRadius = 20;card.layer.borderWidth = 1;card.layer.borderColor = [UIColor colorWithWhite:1 alpha:0.3].CGColor;return card;
}
//卡片標題
- (UILabel *)createSectionTitleLabel:(NSString *)title frame:(CGRect)frame {UILabel *label = [[UILabel alloc] initWithFrame:frame];label.text = title;label.font = [UIFont systemFontOfSize:20 weight:UIFontWeightBold];label.textColor = [UIColor whiteColor];return label;
}
我的背景可以根據天氣不同自動切換,代碼如下:
- (void)setBackgroundBasedOnWeather {if (!self.weatherData) return;NSDictionary *current = self.weatherData[@"current"];NSDictionary *condition = current[@"condition"];NSInteger code = [condition[@"code"] integerValue];NSString *bgImageName;// 晴天if (code == 1000) {bgImageName = @"pic1.jpg";}// 多云或陰天else if (code >= 1003 && code <= 1009) {bgImageName = @"pic2.jpg";}// 雨(小雨~大雨)else if ((code >= 1063 && code <= 1087) || (code >= 1150 && code <= 1195) || (code >= 1240 && code <= 1246)) {bgImageName = @"pic3.jpg";}// 雪(小雪~暴雪)else if ((code >= 1066 && code <= 1074) || (code >= 1114 && code <= 1237)) {bgImageName = @"pic4.jpg";}// 雷暴、雨夾雪等惡劣天氣else if (code >= 1273 && code <= 1282) {bgImageName = @"pic5.jpg";}// 默認else {bgImageName = @"pic1.jpg";}self.backgroundImageView.image = [UIImage imageNamed:bgImageName];
}
//日期字符串轉換為周幾
- (NSString *)formatDate:(NSString *)dateString {NSDateFormatter *inputFormatter = [[NSDateFormatter alloc] init];inputFormatter.dateFormat = @"yyyy-MM-dd";NSDate *date = [inputFormatter dateFromString:dateString];NSDateFormatter *outputFormatter = [[NSDateFormatter alloc] init];outputFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"zh_CN"];outputFormatter.dateFormat = @"EEE";return [outputFormatter stringFromDate:date];
}
詳情頁的刪除按鈕:
- (void)deleteCity {if (self.cityName) {NSDictionary *userInfo = @{@"cityName": self.cityName};[[NSNotificationCenter defaultCenter] postNotificationName:@"DeleteCityNotification"object:niluserInfo:userInfo];[self dismissViewControllerAnimated:YES completion:nil];}
}
關于多個城市橫向滑動查看詳情,我使用了UIPageController。
- (id)init {self = [super initWithTransitionStyle:UIPageViewControllerTransitionStyleScrollnavigationOrientation:UIPageViewControllerNavigationOrientationHorizontaloptions:nil];return self;
}
這段代碼實現了頁面的橫向滑動效果。
// MainVC.m- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {[tableView deselectRowAtIndexPath:indexPath animated:YES];NSMutableArray *cityNames = [NSMutableArray array];for (NSDictionary *city in self.cityData) {[cityNames addObject:city[@"name"]];}CitiesDetailViewController *containerVC = [[CitiesDetailViewController alloc] init];containerVC.cityNames = cityNames;containerVC.initialIndex = indexPath.section;containerVC.modalPresentationStyle = UIModalPresentationFullScreen;[self presentViewController:containerVC animated:YES completion:nil];
}
// CitiesDetailViewController.m- (void)viewDidLoad {[super viewDidLoad];self.delegate = self;self.dataSource = self;self.cities = self.cityNames; // 城市名列表由外部傳入self.currentIndex = self.initialIndex; // 初始城市索引也由外部設置DetailViewController *initialVC = [self detailViewControllerForIndex:self.initialIndex];[self setViewControllers:@[initialVC] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
#pragma mark - Page View Controller Data Source
// 獲取前一個VC
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {//下標NSInteger index = [(DetailViewController *)viewController index];//不是第一個就去上一個if (index > 0) {return [self detailViewControllerForIndex:index - 1];}return nil;
}//獲取后一個VC
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {NSInteger index = [(DetailViewController *)viewController index];if (index < self.cities.count - 1) {return [self detailViewControllerForIndex:index + 1];}return nil;
}//進入指定頁面
- (DetailViewController *)detailViewControllerForIndex:(NSInteger)index {if (index < 0 || index >= self.cities.count) return nil;DetailViewController *detailVC = [[DetailViewController alloc] init];detailVC.cityName = self.cities[index];detailVC.index = index;detailVC.canAddCity = NO;return detailVC;
}- (void)deleteCurrentCity {if (self.cities.count == 0) {[self dismissViewControllerAnimated:YES completion:nil];return;}// 獲取當前顯示的視圖控制器DetailViewController *currentVC = (DetailViewController *)self.viewControllers.firstObject;NSInteger currentIndex = currentVC.index;// 防止索引越界if (currentIndex >= self.cities.count) {[self dismissViewControllerAnimated:YES completion:nil];return;}NSString *cityName = self.cities[currentIndex];// 發送刪除通知NSDictionary *userInfo = @{@"cityName": cityName};[[NSNotificationCenter defaultCenter] postNotificationName:@"DeleteCityNotification"object:niluserInfo:userInfo];// 從數據源中刪除NSMutableArray *mutableCities = [self.cities mutableCopy];[mutableCities removeObjectAtIndex:currentIndex];self.cities = [mutableCities copy];// 如果刪除后沒有城市了,關閉頁面if (self.cities.count == 0) {[self dismissViewControllerAnimated:YES completion:nil];return;}// 計算新的當前索引NSInteger newIndex;if (currentIndex >= self.cities.count) {newIndex = self.cities.count - 1;} else {newIndex = currentIndex;}// 確定導航方向:如果刪除的是最后一個,則向前翻一頁,否則保持當前頁UIPageViewControllerNavigationDirection direction = UIPageViewControllerNavigationDirectionForward;if (newIndex < currentIndex) {direction = UIPageViewControllerNavigationDirectionReverse;}// 獲取新的視圖控制器DetailViewController *newVC = [self detailViewControllerForIndex:newIndex];if (newVC) {[self setViewControllers:@[newVC]direction:directionanimated:YEScompletion:nil];self.currentIndex = newIndex;}
}