
天气预报仿写总结本次项目是仿照iOS原生的天气预报进行仿写, 主要功能包括城市管理, 城市天气数据的展示, 城市搜索,收藏夹页面收藏城市滑动浏览等在这仿写中, 主要采用了MVC的设计模式,笔者认识到了之前对于MVC的应用有很多错误的地方,这次仿写也做了改正天气预报项目是笔者第一次接触网络请求的相关知识,所以开始的时候没有头绪,用好长时间去理解网络请求的步骤, 具体可以看笔者的上一篇博客.在天气预报项目中, 笔者调用的是Open‑Meteo 天气 API, 这个API查询天气不能直接通过城市名称查询, 而是需要通过经纬度查询, 所以我们在查询的时候, 需要先通过城市名称来获取对应城市的经纬度,然后再根据经纬度来定位城市进而获取天气信息搜索这里是根据城市名获取经纬度的APIhttps://geocoding-api.open-meteo.com/v1/search?nameXi%27ancount10languagezhformatjson通过这个API可以对进行模糊搜索, 可以得到城市的名称,经纬度以及所在省份等信息这里搜索笔者是采用了UISearchController, 单独创建了结果更新的视图控制器, 来展示搜索结果, 并在每一次搜索之后更新用于展示结果的UITableView, 关于UISearchController的具体功能笔者之前写过一篇博客专门学习创建UISearchController, 设置搜索结果控制器,homeView.h-(void)setUpNavigation{SearchViewController*vc[[SearchViewController alloc]init];self.searchController[[UISearchController alloc]initWithSearchResultsController:vc];self.searchController.searchResultsUpdatervc;// 模糊背景self.searchController.obscuresBackgroundDuringPresentationYES;// 隐藏导航栏self.searchController.hidesNavigationBarDuringPresentationYES;// 占位文字self.searchController.searchBar.placeholder输入城市名进行搜索;self.searchController.searchBar.delegateself;// 将searchBar 添加到导航栏self.navigationItem.searchControllerself.searchController;// 搜索成一个按钮,点击展开搜索栏// self.navigationItem.preferredSearchBarPlacement UINavigationItemSearchBarPlacementIntegratedButton;self.searchController.delegateself;}在搜索框中输入字符,就会调用搜索结果控制器的- (void)updateSearchResultsForSearchController:(UISearchController *)searchController方法来更新用于结果显示的UITableView,SearchViewController.m-(void)updateSearchResultsForSearchController:(UISearchController*)searchController{self.searchTextsearchController.searchBar.text;if(self.searchText.length0){[self.searchModel.cityArray removeAllObjects];[self.searchView.tableView reloadData];}// // 取消之前尚未执行的延迟调用[NSObject cancelPreviousPerformRequestsWithTarget:selfselector:selector(performSearch)object:nil];// 延迟 0.5 秒后执行实际搜索[selfperformSelector:selector(performSearch)withObject:nil afterDelay:0.5];}-(void)performSearch{if(self.searchText.length0){[self.searchModel.cityArray removeAllObjects];[self.searchView.tableView reloadData];}else{[selfcreateURL];}return;}在这里取消之前的延迟和延迟调用是为了给搜索添加防抖处理用户在搜索框连续输入时会频繁触发updateSearchResultsForSearchController:方法。为防止网络请求过载及响应数据错乱项目中采取了双重防护策略延迟执行使用performSelector:withObject:afterDelay:延迟 0.5 秒执行搜索。若在 0.5 秒内用户继续输入通过cancelPreviousPerformRequests取消前一次未执行的计划任务从而只发起最后一次有效请求。请求 ID 比对忽略过期响应在createURL中生成递增的currentRequestID并在网络请求回调中比对当前 ID。若回调的 ID 与最新 ID 不符则直接丢弃该结果例如,在搜索框中连续输入,每次输入搜索框中的内容发生变化,都会触发搜索结果控制器的UISearchResultsUpdaing协议的- (void)updateSearchResultsForSearchController:(UISearchController *)searchController方法 , 在方法中进行网络请求获取数据从而刷新用于结果显示的UITableView,但是网络请求是异步并发的, 在搜索的时候, 先请求城市A, 后请求城市B, 如果B的服务器响应更快, (比如缓存命中, 数据库查询更简单), B结果会比A更早返回, 于是就会导致A结果后返回覆盖A结果// // 取消之前尚未执行的延迟调用[NSObject cancelPreviousPerformRequestsWithTarget:selfselector:selector(performSearch)object:nil];// 延迟 0.5 秒后执行实际搜索[selfperformSelector:selector(performSearch)withObject:nil afterDelay:0.5];这两段代码中[NSObject cancelPreviousPerformRequestsWithTarget:self selector:selector(performSearch) object:nil];的作用是取消还没有发出的请求操作假设 当输入“北京”后这行代码会将performSearch这个任务延迟 0.5 秒放入一个本地定时器RunLoop。此时网络请求并没有真正发出。如果 0.3 秒后你又输入了“京”字变成“北京”cancelPreviousPerformRequests就会把上一个还没到 0.5 秒的定时任务从本地直接删掉。结果第一个performSearch永远不会被执行网络请求也就不会被创建和发送。所以这句话的本质是“取消即将要执行的操作省下这次不必要的网络请求”而不是“忽略已经发出去的回调”。而[self performSelector:selector(performSearch) withObject:nil afterDelay:0.5];的作用是忽略已经发出的但是返回较晚的旧请求下面是调用自定义的网络管理单例创建网络请求, 然后用请求到的数据刷新结果显示-(void)createURL{// 增加请求IDself.currentRequestID;// 保存本次请求的 IDNSInteger requestIDself.currentRequestID;[[NetworkManager sharedManager]GET:https://geocoding-api.open-meteo.com/v1/searchparameters:{name:self.searchText,count:10,language:zh,format:json}completion:^(NSDictionary*_Nullable json,NSError*_Nullable error){// 检查本次回调是否属于当前的最新请求if(requestID!self.currentRequestID){// 与本次请求不一致, 不响应return;}if(!errorjson){NSArray*resultsjson[results];[self.searchModel.cityArray removeAllObjects];if(results){[self.searchModel.cityArray addObjectsFromArray:results];}[self.searchView.tableView reloadData];}}];}调用API返回的结果是JSON格式的数据(网络响应体中的字符串),NetworkManager会将数据解析为NSDictionary对象, 并以回调参数 JSON 的形式返回给调用方 , 在代码块中再对解析后的数据进行操作,更新数据源dataSource-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath{UITableViewCell*cell[tableView dequeueReusableCellWithIdentifier:UITableViewCellIDforIndexPath:indexPath];NSDictionary*cityInfoself.searchModel.cityArray[indexPath.row];cell.textLabel.text[NSString stringWithFormat:% -- %,cityInfo[name],cityInfo[admin1]];// cell.textLabel.text self.searchText;returncell;}这是cell根据更新的数据源显示的数据,包括城市的名称和所在省份这里我调用自定义的网络管理单例的GET方法, 并处理了UI的更新然后点击搜索结果的cell可以进行跳转;-(void)tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath{[tableView deselectRowAtIndexPath:indexPath animated:YES];NSDictionary*cityInfoself.searchModel.cityArray[indexPath.row];CGFloat latitude[cityInfo[latitude]doubleValue];CGFloat longitude[cityInfo[longitude]doubleValue];UITableViewCell*cell[tableView cellForRowAtIndexPath:indexPath];CityModel*city[[CityModel alloc]initWithName:cell.textLabel.text Latitude:(latitude)Longitude:(longitude)];WeatherController*vc[[WeatherController alloc]init];vc.citycity;UINavigationController*Nav[[UINavigationController alloc]initWithRootViewController:vc];self.navigationItem.searchController.searchBar.text;[selfpresentViewController:Nav animated:YES completion:nil];}当点击cell的时候,就会获取对应城市的经纬度, 传值到天气详情界面并跳转详情展示在天气详情界面,通过获取到的经纬度继续创建网络请求, 查询城市天气WeatherPageController.m-(void)createURL{NSLog(weather 发出了网络请求);[[NetworkManager sharedManager]GET:https://api.open-meteo.com/v1/forecastparameters:{latitude:(self.city.latitude),longitude:(self.city.longitude),daily:temperature_2m_max,temperature_2m_min,sunrise,sunset,precipitation_sum,wind_speed_10m_max,wind_direction_10m_dominant,weather_code,uv_index_max,hourly:temperature_2m,precipitation,snowfall,weather_code,wind_speed_10m,wind_direction_10m,current:temperature_2m,is_day,precipitation,weather_code,wind_speed_10m,wind_direction_10m,timezone:Europe/Moscow}completion:^(NSDictionary*_Nullable json,NSError*_Nullable error){if(json[current]json[daily]json[hourly]){NSLog(请求到了数据);self.weatherModel.CurrentWeatherModeljson[current];self.weatherModel.DailyWeatherModeljson[daily];self.weatherModel.HourlyWeatherModeljson[hourly];[selfsetUpInterface];[self.weatherView configWithCurrentWeather:self.weatherModel.CurrentWeatherModel];[self.weatherView.tableView reloadData];}else{NSLog(加载失败);NSLog(%,error);UIAlertController*alertController[UIAlertController alertControllerWithTitle:nil message:加载失败, 请检查网络preferredStyle:UIAlertControllerStyleAlert];UIAlertAction*okAction[UIAlertAction actionWithTitle:确定style:UIAlertActionStyleDefault handler:^(UIAlertAction*_Nonnull action){[selfpressBack];}];[alertController addAction:okAction];[selfpresentViewController:alertController animated:YES completion:nil];}}];}在展示页面,笔者使用UITableView自定义cell来显示, 主要分为几个板块当前天气, 未来24小时天气, 未来7天天气这些数据都是通过API返回的,NetworkManage将返回的JSON数据解析为NSDictionary对象, 我们可以直接接收并直接从字典中获取收藏城市的添加和展示要做到在详情展示界面可以添加城市到收藏栏,还要在首页显示收藏的城市信息, 这里笔者使用了全局单例,创建了一个收藏夹类来存储收藏的城市当在详情界面点击添加按钮的时候,就会在收藏夹中添加一个城市信息,包括城市的名称, 所在省份以及经纬度同时在点击添加按钮的时候就会通过通知传值让首页显示添加的城市,避免了返回首页再进行网络请求获取数据导致显示异常-(void)pressAdd{HomeModel*homeModel[HomeModel shareInstance];if(!homeModel.homeCities){homeModel.homeCities[[NSMutableArray alloc]init];}if([homeModel.homeCities indexOfObject:self.city]NSNotFound){[homeModel addCityToSave:self.city];self.navigationItem.rightBarButtonItemself.deleteButton;UIAlertController*alertController[UIAlertController alertControllerWithTitle:nil message:添加成功preferredStyle:UIAlertControllerStyleAlert];UIAlertAction*okAction[UIAlertAction actionWithTitle:确定style:UIAlertActionStyleDefault handler:^(UIAlertAction*_Nonnull action){[[NSNotificationCenter defaultCenter]postNotificationName:ReleadNotification object:selfuserInfo:nil];NSLog(OK);}];[alertController addAction:okAction];[selfpresentViewController:alertController animated:YES completion:nil];}else{UIAlertController*alertController[UIAlertController alertControllerWithTitle:nil message:该城市已经添加收藏夹, 重复添加preferredStyle:UIAlertControllerStyleAlert];UIAlertAction*okAction[UIAlertAction actionWithTitle:确定style:UIAlertActionStyleDefault handler:^(UIAlertAction*_Nonnull action){NSLog(OK);}];[alertController addAction:okAction];[selfpresentViewController:alertController animated:YES completion:nil];}}