天气预报总结

天气预报总结

文章目录

前言

我们的APP不可能永远使用本地数据,网络请求作为大多数APP的硬性要求,需要在软件里实现API调用,JSON数据解析,在我们的天气预报里,我们要实现城市搜索和数据的刷新,所以这次仿写主要考察的就是UI的排版和网络请求,设计模式主要为MVC

异步处理

iOS 应用的所有 UI 更新(刷新 TableView、响应点击、滚动列表)都发生在主线程上。而网络请求可能耗时数秒,应用无法响应用户操作,系统会判定应用"无响应"并弹出"卡死"提示,用户体验极差

异步处理的作用:将耗时任务(网络请求、文件读写、大量计算)放到后台线程执行,主线程继续处理 UI 事件。等任务完成后,再将结果传递回主线程更新 UI

objc 复制代码
dispatch_async(dispatch_get_main_queue(), ^{
          self.cityArray = array;
          self.collection.hidden = NO;
          self.tableView.hidden = YES;
          [self.collection reloadData];
      });

首页

首页需要展示的是已添加城市的天气卡片和搜索框,并支持添加城市到首页,对于卡片和搜索列表我们使用cell就行,里面把我们需要的城市温度等等数据放进去,想要一个城市的数据,就得通过相关的信息查询,城市名称不适合我们精准查找但适合模糊搜索,所以我们的搜索功能就是先通过searchController里的文字查询(使用专门的查询api)得到经纬度(最精准),再请求详细数据,

objc 复制代码
- (void) requestSearch:(NSString *) city {
    NSString *cityEncode = [city stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];

    NSString *urlString = [NSString stringWithFormat:@"https://geocoding-api.open-meteo.com/v1/search?name=%@&count=10&language=zh&format=json", cityEncode];
    NSURL *url = [NSURL URLWithString:urlString];
    NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (error) {
            NSLog(@"%@", error);
            return;
        }
        NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
        NSArray *results = dict[@"results"];
        NSMutableArray *array = [NSMutableArray array];
        for (NSDictionary *item in results) {
            cityModel *model = [[cityModel alloc] init];
            model.cityname = item[@"name"];
            model.latitude = item[@"latitude"];
            model.longitude = item[@"longitude"];
            model.country = item[@"country"];
            model.admin1 = item[@"admin1"];
            [array addObject:model];
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            self.cityArray = array;
            self.collection.hidden = NO;
            self.tableView.hidden = YES;
            [self.collection reloadData];
        });
    }];
    [task resume];
}

搜索要注意的就是cell是搜索框输入时自己会刷新展示出来,而在我们决定添加某个城市时要自动清除输入框里的文字,不然cell还是会根据框内文字刷新遮盖视图

而对于我们的天气卡片,就要使用搜索获得的经纬度查询

objc 复制代码
- (void) requestWeather : (cityModel *)city{
    NSString *urlString = [NSString stringWithFormat:
    @"https://api.open-meteo.com/v1/forecast?latitude=%@&longitude=%@&current=temperature_2m,weather_code&hourly=temperature_2m,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=auto", city.latitude, city.longitude];
    NSURL *url = [NSURL URLWithString:urlString];
    NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (error) {
            NSLog(@"%@", error);
            return;
        }
        NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
        WeatherModel *model = [[WeatherModel alloc] init];
        NSDictionary *current = dict[@"current"];
        model.city = city.cityname;
        model.tem = [NSString stringWithFormat:@"%@℃", dict[@"current"][@"temperature_2m"]];
        NSNumber *code = current[@"weather_code"];
        NSInteger weatherCode = [code integerValue];
        
        if (weatherCode == 0) {
            model.weather = @"晴";
        } else if (weatherCode == 1 || weatherCode == 2) {
            model.weather = @"多云";
        } else if (weatherCode == 3) {
            model.weather = @"阴";
        } else if ((weatherCode >= 45 && weatherCode <= 48)) {
            model.weather = @"阴";
        } else if ((weatherCode >= 51 && weatherCode <= 67) || (weatherCode >= 80 && weatherCode <= 82) || (weatherCode >= 95 && weatherCode <= 99)) {
            model.weather = @"雨";
        } else if ((weatherCode >= 71 && weatherCode <= 77) || weatherCode == 85 || weatherCode == 86) {
            model.weather = @"雪";
        } else {
            model.weather = @"阴";
        }
        model.time = [NSString stringWithFormat:@"%@", current[@"time"]];
        dispatch_async(dispatch_get_main_queue(), ^{
            BOOL exist = NO;
            for (WeatherModel *m in self.weatherArray) {
                if ([m.latitude isEqualToNumber:model.latitude] && [m.longitude isEqualToNumber:model.longitude]) {
                    exist = YES;
                    break;
                }
            }
            if (!exist) {
                [self.weatherArray addObject:model];
            }
            self.cityArray = @[];
            self.searchController.searchBar.text = @"";
            self.searchController.active = NO;
            [self.tableView reloadData];
        });
    }];
    [task resume];
}

在我们得到weather_code要挂定天气范围方便展示,同时也可以根据文字决定背景图,在主线程回调时还要进行是否有重复城市添加的检查,再清空输入框里的文字

在首页的cell排版时我们看我能会遇到中间没有间隔的问题,给header一个空白View就行了

天气卡片还要实现左滑删除的功能

objc 复制代码
- (UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath {
    UIContextualAction *deleteAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive title:@"删除" handler:^(UIContextualAction *action, UIView *sourceView, void (^completionHandler)(BOOL)) {
        [self.weatherArray removeObjectAtIndex:indexPath.section];
        NSIndexSet *sections = [NSIndexSet indexSetWithIndex:indexPath.section];
        [tableView deleteSections:sections withRowAnimation:UITableViewRowAnimationAutomatic];
        completionHandler(YES);
    }];
    UISwipeActionsConfiguration *configuration = [UISwipeActionsConfiguration configurationWithActions:@[deleteAction]];
    configuration.performsFirstActionWithFullSwipe = NO;
    return configuration;
}

这样左滑会出现一个按钮,继续左滑可以直接删除

详情页面

我们点进一个卡片或者搜索就会展示详情页面,我们在首页使用present,搜索页面使用push,首页使用present是因为我们需要在几个城市的详情页面里实现滑动切换的功能,另外使用pageController实现底部小白点的展示

objc 复制代码
UIPageViewController *pageVC = [[UIPageViewController alloc] 
    initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll 
    navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal 
    options:nil];
pageVC.dataSource = self;
pageVC.delegate = self;
[self addChildViewController:pageVC];
[self.view addSubview:pageVC.view];
pageVC.view.frame = self.view.bounds;
[pageVC didMoveToParentViewController:self];
self.pageController = pageVC;
objc 复制代码
self.pageControl = [[UIPageControl alloc] init];
self.pageControl.numberOfPages = self.pages.count;
self.pageControl.currentPage = self.currentIndex;
self.pageControl.pageIndicatorTintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.4];
self.pageControl.currentPageIndicatorTintColor = UIColor.whiteColor;
[self.view addSubview:self.pageControl];
[self.pageControl mas_makeConstraints:^(MASConstraintMaker *make) {
    make.centerX.equalTo(self.view);
    make.bottom.equalTo(self.view.mas_bottom).offset(-15);
}];

在点击任意卡片的时候一定要保证索引被正确传递,否则只能进去pages里的第一个界面不符合要求

objc 复制代码
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    pageViewController *vc = [[pageViewController alloc] init];
    vc.weatherArray = self.weatherArray;
    vc.currentIndex = indexPath.section;
    [self presentViewController:vc animated:YES completion:nil];
}

那么我们从首页给pageView传值后还不够,还要pageView给我们的详情页传经纬度方便详情页刷新各种数据,这里要注意的是我们不需要从首页传所有的数据,其他数据可以在页面进去后再请求,这样可以减少不跟手的情况增加流畅度

objc 复制代码
for (WeatherModel *model in self.weatherArray) {
        cardViewController *vc = [[cardViewController alloc] init];
        vc.city = model.city;
        vc.latitude = model.latitude;
        vc.longitude = model.longitude;
        [self.pages addObject:vc];
    }

那么现在我们就到了详情页的内部,这里我们要实现苹果天气预报一样的视觉效果,就要使用iOS的毛玻璃

objc 复制代码
- (UIVisualEffectView *)createBlurView {
    UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleSystemUltraThinMaterialLight];
    UIVisualEffectView *blurView = [[UIVisualEffectView alloc] initWithEffect:effect];
    blurView.alpha = 0.9;
    return blurView;
}

UIBlurEffect:模糊效果的核心

它是一个视觉效果描述对象,定义了模糊的样式(如 SystemUltraThinMaterialLight),但不直接渲染。

系统会基于其背后内容的颜色、亮度等信息,实时计算并生成动态模糊效果

UIVisualEffectView:承载模糊效果的容器

它有两个关键的层:

UIVisualEffectView 自身负责渲染模糊背景。

contentView是它的子视图容器,所有需要显示在毛玻璃之上的 UI 控件(文字、图片等)都应添加到 contentView 中,以确保它们显示在模糊层上方。

blurView 覆盖在其他视图上时,背后的内容会被实时模糊处理。

UIBlurEffectStyleSystemUltraThinMaterialLight

这是 iOS 13+ 引入的材质化样式,会根据系统深色/浅色模式自动适配:

浅色模式:呈现浅色半透明毛玻璃

深色模式:呈现深色半透明毛玻璃

样式 效果
UIBlurEffectStyleExtraLight 极浅色模糊
UIBlurEffectStyleLight 浅色模糊
UIBlurEffectStyleDark 深色模糊
UIBlurEffectStyleRegular 常规材质(适配深/浅色模式)
UIBlurEffectStyleProminent 突出材质

将内容添加到contentView上就可以实现毛玻璃效果了

我们的这些数据部分里几个需要单独拿出来讲

  1. 渐变视图
objc 复制代码
self.gradientLayer = [CAGradientLayer layer];
self.gradientLayer.colors = @[
    (__bridge id)[UIColor systemGreenColor].CGColor, 
    (__bridge id)[UIColor systemOrangeColor].CGColor
];
self.gradientLayer.startPoint = CGPointMake(0, 0.5);
self.gradientLayer.endPoint = CGPointMake(1, 0.5);
self.gradientLayer.cornerRadius = 1;
[self.temLine.layer addSublayer:self.gradientLayer];

CAGradientLayer 是 专门用于绘制颜色渐变的图层类。它是 CALayer 的子类

colors 渐变的颜色数组,定义渐变中使用的颜色序列,至少需要两个颜色

locations颜色分布的位置,第一个颜色必须是0.0,最后一个必须为1.0

startPointendPoint 渐变的方向与范围,水平渐变:(0, 0.5) → (1, 0.5),垂直渐变:(0.5, 0)→ (0.5, 1),对角线渐变:(0, 0)→ (1, 1)

在比较最近一周高低温的时候会用到,再通过数学计算实现渐变条的长短变化,就可以有和苹果差不多的效果

  1. 旋转视图
objc 复制代码
self.img2.transform = CGAffineTransformMakeRotation(direction * M_PI / 180.0);

CGAffineTransform 是 Core Graphics 框架中用于仿射变换的结构体,所有变换都相对于视图的 centeranchorPoint进行

这里我也只会用它来实现旋转,其他的并未了解,括号里的就是我们旋转的弧度

这样可以实现风向的可视化,通过箭头的转向实现

  1. 曲线

使用 UIBezierPath 绘制路径

objc 复制代码
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(10, 110)];
[path addQuadCurveToPoint:CGPointMake(160, 110) controlPoint:CGPointMake(90, 45)];

(160, 110) 为终点,(90, 45) 为控制点

将路径渲染到屏幕

objc 复制代码
self.sunLayer = [CAShapeLayer layer];
self.sunLayer.path = path.CGPath;
self.sunLayer.strokeColor = [UIColor whiteColor].CGColor;
self.sunLayer.fillColor = UIColor.clearColor.CGColor;
self.sunLayer.lineWidth = 2;
[self.contentView.layer addSublayer:self.sunLayer];

CAShapeLayer 负责渲染路径,由 GPU 加速

strokeColor 设置线条颜色为白色

fillColor = clear 表示不填充内部,只描边

这样一条曲线就出现在屏幕上了,后面创建一个小白点结合日升日落时间在这条线上移动就可以实现太阳的可视化了

那么我们在写详情界面的时候尽量使用一个collectionView并复用,优化性能,另外在使用pages翻页的时候,我们可以提前把本页的上一页与下一页给加载出来,切换时视觉上更流畅

总结

本次天气预报涉及了网络请求,MVC架构和更多UI的学习,让我们开始能体会到用户体验的重要性

相关推荐
xixiaoyunya5 小时前
Mac远程访问新范式:基于80KM穿云箭的内网穿透技术实践
macos
秋雨梧桐叶落莳7 小时前
计算器仿写总结
学习·macos·ios·objective-c·cocoa
末代iOS程序员华仔15 小时前
iOS上架海外工具类应用合规指南:避免封号与下架风险
flutter·ios·swift
寒水馨15 小时前
macOS下载、安装uv-0.12.0(附安装包uv-aarch64-apple-darwin.tar.gz)
python·macos·rust·项目管理·包管理器·astral·pip替代
寒水馨16 小时前
macOS下载、安装protobuf-v35.1(附安装包protoc-35.1-osx-universal_binary.zip)
macos·google·grpc·序列化·protobuf·protoc·数据交换
寒水馨16 小时前
macOS下载、安装ripgrep-15.2.0(附安装包ripgrep-15.2.0-aarch64-apple-darwin.tar.gz)
macos·正则表达式·rust·ripgrep·rg·命令行搜索·代码搜索
末代iOS程序员华仔17 小时前
Flutter 开发 iOS App 上架全流程与常见问题分析
flutter·ios
末代iOS程序员华仔17 小时前
Flutter开发iOS海外工具类应用上架合规指南与风险规避
flutter·ios
AI云海1 天前
iOS 多 Target 打包、UAT/生产测试与上架 App Store 全流程指南
ios