「iOS」天气预报仿写总结
文章目录
首页
在首页使用了一个tableView来展示已添加的城市卡片,使用一个数组来存储添加的model,获取数据则是先通过一个简单的地理API先通过城市名来获取经纬度再通过经纬度获取当地的天气情况
objc
-(void)getWeather:(double)latitude longitude:(double)longitude cityname:(NSString*)city
{
NSString *urlStr = [NSString stringWithFormat:@"https://api.open-meteo.com/v1/forecast?latitude=%f&longitude=%f¤t=temperature_2m,weather_code,uv_index,relative_humidity_2m,apparent_temperature,wind_direction_10m,wind_speed_10m,surface_pressure&hourly=temperature_2m,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset&timezone=auto", latitude, longitude];
NSURL*url = [NSURL URLWithString:urlStr];
NSURLSessionDataTask*task = [[NSURLSession sharedSession]dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if(error){
NSLog(@"请求失败");
return;
}
NSError*err;
NSDictionary*dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:&err];
NSDictionary*currentDict = dict[@"current"];
CGFloat tempValue = [currentDict[@"temperature_2m"] floatValue];
NSString *nowTemp = [NSString stringWithFormat:@"%.0f°", tempValue];//现在温度
NSInteger weatherCode = [currentDict[@"weather_code"] integerValue];
NSString*weather = [self weatherCodeToString:weatherCode];//天气
CGFloat uv = [currentDict[@"uv_index"] floatValue];
NSString *sunray = [NSString stringWithFormat:@"%.1f", uv];
CGFloat hum = [currentDict[@"relative_humidity_2m"] floatValue];
NSString *damp = [NSString stringWithFormat:@"%.0f%%", hum];
CGFloat feelTemp = [currentDict[@"apparent_temperature"] floatValue];
NSString *bodytemp = [NSString stringWithFormat:@"%.0f°", feelTemp];
CGFloat windDir = [currentDict[@"wind_direction_10m"] floatValue];
NSString *windDire = [NSString stringWithFormat:@"%.0f°", windDir];
CGFloat windSp = [currentDict[@"wind_speed_10m"] floatValue];
NSString *windSpeed = [NSString stringWithFormat:@"%.1f km/h", windSp];
CGFloat pressure = [currentDict[@"surface_pressure"] floatValue];
NSString *gasPres = [NSString stringWithFormat:@"%.0f hPa", pressure];
NSDictionary*dailyDict = dict[@"daily"];
NSArray*maxTempArr = dailyDict[@"temperature_2m_max"];
NSArray*minTempArr = dailyDict[@"temperature_2m_min"];
NSString*todayHigh = maxTempArr.firstObject;//最高温
NSString*todayLow = minTempArr.firstObject;//最低温
NSArray*sunrises = dailyDict[@"sunrise"];
NSArray*sunsets = dailyDict[@"sunset"];
NSString*sunriseStr = sunrises.firstObject;
NSString*sunsetStr = sunsets.firstObject;
if(sunriseStr.length >= 16){
sunriseStr = [sunriseStr substringWithRange:NSMakeRange(11, 5)];
}
if (sunsetStr.length >= 16) {
sunsetStr = [sunsetStr substringWithRange:NSMakeRange(11, 5)];
}
NSDateFormatter *isoFormatter = [[NSDateFormatter alloc] init];
isoFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm";
isoFormatter.timeZone = [NSTimeZone localTimeZone];
NSDate *nowDate = [NSDate date];
NSDate *end24HDate = [nowDate dateByAddingTimeInterval:24 * 60 * 60];
NSDictionary *daily = dict[@"daily"];
NSDictionary *hourly = dict[@"hourly"];
NSMutableArray *hours = [NSMutableArray array];
NSArray *times = hourly[@"time"];
NSArray *temps = hourly[@"temperature_2m"];
NSArray *codes = hourly[@"weather_code"];
NSInteger hourCount = MIN(times.count, MIN(temps.count, codes.count));
// 当前 ~ 24小时之内的数据
for (NSInteger i = 0; i < hourCount; i++) {
NSString *timeStr = times[i];
NSDate *itemDate = [isoFormatter dateFromString:timeStr];
if (!itemDate) continue;
// 只取大于等于现在并且小于等于24小时后
if ([itemDate compare:nowDate] != NSOrderedAscending && [itemDate compare:end24HDate] != NSOrderedDescending) {
NSDictionary *dic = @{
@"time": times[i],
@"temp": temps[i],
@"code": codes[i]
};
[hours addObject:dic];
}
}
NSMutableArray *days = [NSMutableArray array];
NSArray *dates = daily[@"time"];
NSArray *maxTemps = daily[@"temperature_2m_max"];
NSArray *minTemps = daily[@"temperature_2m_min"];
NSArray *weatherCodes = daily[@"weather_code"];
NSInteger dayCount = MIN(dates.count, MIN(maxTemps.count, MIN(minTemps.count, weatherCodes.count)));
// 最多只拿7条
NSInteger realDayNum = dayCount > 7 ? 7 : dayCount;
for (NSInteger i = 0; i < realDayNum; i++) {
NSDictionary *dic = @{
@"date": dates[i],
@"max": maxTemps[i],
@"min": minTemps[i],
@"code": weatherCodes[i]
};
[days addObject:dic];
}
CityWeatherModel*model = [[CityWeatherModel alloc]init];
model.city = city;
model.temp = nowTemp;
model.weather = weather;
model.maxTemp = todayHigh;
model.minTemp = todayLow;
model.latitude = latitude;
model.longitude = longitude;
model.hourList = hours;
model.dailyList = days;
model.sunray = sunray;
model.damp = damp;
model.bodyTemp = bodytemp;
model.windDire = windDire;
model.windSpeed = windSpeed;
model.gasPres = gasPres;
model.sunRise = sunriseStr;
model.sunSet = sunsetStr;
dispatch_async(dispatch_get_main_queue(), ^{
[self.dataArr addObject:model];
[self.tab reloadData];
});
}];
[task resume];
}
同时首页卡片还要有删功能,本质是删除数组里的相应model,这里使用UITableview的方法
objc
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
return UITableViewCellEditingStyleDelete;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if(editingStyle == UITableViewCellEditingStyleDelete){
[self.dataArr removeObjectAtIndex:indexPath.section];
[tableView reloadData];
}
}
- (NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath
{
return @"删除";
}

搜索页
搜索页则是UITextField与UITableView组成,依旧是通过网络请求获取城市;输入地名后会依据城市名搜索到城市,由于后续需要进入详细页所以顺便获取存储经纬度到model,再将model存入数组,依靠数组来填充cell内容
objc
-(void)requestSearchCity:(NSString*)key
{
NSString *encodeKey = [key stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
NSString*urlString = [NSString stringWithFormat:@"https://geocoding-api.open-meteo.com/v1/search?name=%@&count=10&language=zh&format=json",encodeKey];
NSURL*url = [NSURL URLWithString:urlString];
NSURLSessionDataTask*task = [[NSURLSession sharedSession]dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if(error){
NSLog(@"地理请求失败",error);
return ;
}
NSError*jsonErr;
NSDictionary*jsonDict = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonErr];
if (jsonErr) return;
NSArray *resArr = jsonDict[@"results"];
[self.cityDataArr removeAllObjects];
for (NSDictionary *dict in resArr) {
CityWeatherModel *model = [[CityWeatherModel alloc] init];
NSString *city = dict[@"name"] ?: @"";
NSString *admin = dict[@"admin1"] ?: @"";
model.city = city;
model.admin = admin;
model.latitude = [dict[@"latitude"] doubleValue];
model.longitude = [dict[@"longitude"] doubleValue];
[self.cityDataArr addObject:model];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.tab reloadData];
});
}];
[task resume];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.cityDataArr.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"tab" forIndexPath:indexPath];
CityWeatherModel *model = self.cityDataArr[indexPath.row];
cell.textLabel.text = [NSString stringWithFormat:@"%@-%@",model.city,model.admin];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
return cell;
}
接下来点击进入对应城市的详细页,我的处理是先获取详细页所要用到的所有数据再跳转页面,这就会导致在点击后不会第一时间跳转而是等待数据请求完毕再跳转,这时需要一个加载界面让使用更舒服
objc
//setupUI
self.loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleLarge];
self.loadingView.hidesWhenStopped = YES;
self.loadingView.color = [UIColor darkGrayColor];
[self.view addSubview:self.loadingView];
[self.loadingView mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.view);
}];
- (void)showLoading {
dispatch_async(dispatch_get_main_queue(), ^{
[self.loadingView startAnimating];
self.view.userInteractionEnabled = NO; // 锁住界面防止重复点击
});
}
- (void)hideLoading {
dispatch_async(dispatch_get_main_queue(), ^{
[self.loadingView stopAnimating];
self.view.userInteractionEnabled = YES;
});
}
//在点击cell后立即调用showLoading,在请求结束后先调用hideLoading再进行数据解析操作

详细页
详细页主要就是添加到首页的操作,首先需要判断首页是否已经添加该城市,若没有添加就可以进行添加操作,这两个操作都使用的代理传值
判断
objc
//先在详细页定义协议
//DetailPage.h
@protocol DetailPageDelegate <NSObject>
// 添加新城市回调
- (BOOL)detailAddNewCity:(CityWeatherModel *)cityModel;
- (BOOL)detailCheckCityExist:(CityWeatherModel *)cityModel;
@end
//在首页实现方法,注意要遵守协议
- (BOOL)detailCheckCityExist:(CityWeatherModel *)cityModel {
for (CityWeatherModel *item in self.dataArr) {
if (fabs(item.latitude - cityModel.latitude) < 0.01 &&
fabs(item.longitude - cityModel.longitude) < 0.01) {
return YES;
}
}
return NO;
}
// 接收新增城市,加入数组并刷新列表
- (BOOL)detailAddNewCity:(CityWeatherModel *)cityModel {
if ([self detailCheckCityExist:cityModel]) {
return NO;
}
[self getWeather:cityModel.latitude longitude:cityModel.longitude cityname:cityModel.city];
return YES;
}
//在详细页判断是否已添加
BOOL isAdded = NO;
if ([self.delegate respondsToSelector:@selector(detailCheckCityExist:)]) {
isAdded = [self.delegate detailCheckCityExist:self.model];
}
if (isAdded) {
// 已添加:文字改为已添加,灰色不可点击
self.rightBtn = [[UIBarButtonItem alloc] initWithTitle:@"已添加" style:UIBarButtonItemStylePlain target:nil action:nil];
self.rightBtn.tintColor = [UIColor lightGrayColor];
} else {
self.rightBtn = [[UIBarButtonItem alloc] initWithTitle:@"添加" style:UIBarButtonItemStylePlain target:self action:@selector(addCard)];
self.rightBtn.tintColor = [UIColor whiteColor];
}
//添加城市
- (void)addCard {
if ([self.delegate respondsToSelector:@selector(detailAddNewCity:)]) {
BOOL success = [self.delegate detailAddNewCity:self.model];
NSLog(@"fuck");
if (success) {
self.rightBtn.title = @"已添加";
self.rightBtn.tintColor = [UIColor lightGrayColor];
self.rightBtn.action = nil;
[self.navigationController popViewControllerAnimated:YES];
}
}
}

首页卡片滑动切换

实现如图所示的切换效果,这里使用了UIPageViewController
objc
- (void)presentPageViewControllerWithInitialIndex:(NSInteger)index {
// 记录当前页码,用于presentationIndexForPageViewController
self.currentPageIndex = index;
NSDictionary *options = @{UIPageViewControllerOptionInterPageSpacingKey:@(30)};
// 初始化分页控制器
// TransitionStyleScroll:滚动翻页
// NavigationOrientationHorizontal:水平左右滑动
self.pageVC = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
options:options];
self.pageVC.dataSource = self;
// 根据下标创建初始要展示的详情页控制器
DetailPage *initialVC = [self detailPageForIndex:index];
// 设置pageVC首个显示的控制器,无动画
[self.pageVC setViewControllers:@[initialVC] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
UIButton *closeBtn = [UIButton buttonWithType:UIButtonTypeSystem];
[closeBtn setTitle:@"✕" forState:UIControlStateNormal];
closeBtn.titleLabel.font = [UIFont boldSystemFontOfSize:24];
closeBtn.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.3];
closeBtn.layer.cornerRadius = 20;
// 点击关闭弹窗
[closeBtn addTarget:self action:@selector(dismissPageVC) forControlEvents:UIControlEventTouchUpInside];
[self.pageVC.view addSubview:closeBtn];
[closeBtn mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.pageVC.view).offset(44);
make.right.equalTo(self.pageVC.view).offset(-20);
make.width.height.mas_equalTo(40);
}];
[self presentViewController:self.pageVC animated:YES completion:^{
self.isPushing = NO;
}];
}
- (void)dismissPageVC {
[self.pageVC dismissViewControllerAnimated:YES completion:nil];
}
- (DetailPage *)detailPageForIndex:(NSInteger)index {
DetailPage *vc = [[DetailPage alloc] init];
// 传递数据源模型
vc.model = self.dataArr[index];
// 记录页面下标,核心:滑动时依靠pageIndex判断页码
vc.pageIndex = index;
vc.delegate = self;
return vc;
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {
// 获取当前页面下标
NSInteger idx = [self indexOfDetailPage:viewController];
// 下标非法 / 当前已经是第0页,没有上一页,返回nil禁止继续滑动
if (idx == NSNotFound || idx == 0) return nil;
// 创建前一页控制器返回
return [self detailPageForIndex:idx - 1];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {
NSInteger idx = [self indexOfDetailPage:viewController];
// 下标非法 / 当前已经是最后一条,没有下一页
if (idx == NSNotFound || idx == self.dataArr.count - 1) return nil;
return [self detailPageForIndex:idx + 1];
}
- (NSInteger)indexOfDetailPage:(UIViewController *)vc {
// 类型安全判断,防止传入其他控制器崩溃
if (![vc isKindOfClass:[DetailPage class]]) return NSNotFound;
DetailPage *detail = (DetailPage *)vc;
return detail.pageIndex;
}
- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController {
return self.currentPageIndex;
}