资讯详情

iOS天气App从零到答辩:SwiftUI+定位+网络请求完整实践

📅 2026/10/9 1:04:51 | 华诺云谱 👁 阅读
iOS天气App从零到答辩:SwiftUI+定位+网络请求完整实践
简介面向iOS平台天气APP设计与实现的毕业设计文献综述适合计算机软件、移动应用方向的高校学生使用在选题论证和文献调研阶段可帮助快速明确研究背景、范围与理论支撑。压缩包内含1个doc文档整体仅48KB内容精炼便于直接阅读、引用与二次编辑。目前已有215人学习/下载对需要了解移动天气应用现状、梳理毕业设计思路的用户具有直接参考价值。文档从移动互联网的简介、发展现状与基本特点切入既阐述移动互联网在终端、软件及应用层面的构成也结合移动用户数量、智能手机保有量和App Store下载量等数据分析天气APP应运而生的市场基础随后梳理基于iOS天气APP的应用现状并归纳出设计实现中应关注的用户体验、数据获取、功能集成、个性化设置与安全性等关键点。文档结构清晰从引言、移动互联网到天气APP应用层层递进可作为开题报告、文献综述撰写及答辩背景介绍的实用素材支撑。1. 基于 iOS 平台的天气 App 应用设计与实现先弄清这个题目的真实边界很多同学看到“基于 iOS 平台的天气 App 应用设计与实现”这个标题第一反应是“画个晴天图标、展示温度和湿度就完事了”。等到开题答辩才发现老师追问的是三件事天气数据从哪里来、用户定位权限怎么给、界面状态怎么在加载和出错之间切换。这个题目真正锻炼的是网络请求、JSON 解析、CoreLocation 定位、异步 UI 更新这四个基本功不是单纯练 UI。下面按“文献综述拆需求—搭建网络层—接入定位—踩坑—答辩验证”的顺序展开让你既能提交一份能自圆其说的文档也能跑出一个真机可演示的 App。适合准备做毕设、又不想被复杂架构劝退的 iOS 开发入门者也适合想快速验证天气类项目可行性的开发者。2. 从文献综述到需求拆解天气 App 的方案选型与技术基线2.1 文献综述先回答三个问题为什么做、用什么做、做到什么程度标题里的.doc不是摆设它提醒我们这个毕设最终要交付一份能讲清“为什么这么做”的文档。文献综述是文档的开局也是最容易被写成“翻译汇总”的部分。我一般会这样组织先不急着下载一堆论文而是带着三个问题去检索。第一个问题移动天气 App 早期怎么实现、现在怎么做回答它就能引出平台演进逻辑第二个问题类似系统一般用什么语言、用什么框架、用什么数据源回答它就能确定自己的技术基线第三个问题现有方案有哪些不足或者哪些点可以优化回答它能让综述有观点而不是流水账。检索范围不用太广。建议以“移动天气应用”“iOS 开发”“天气数据接口”为关键词在中英文数据库先找 68 篇硕士论文再补几篇期刊和官方文档。硕士论文的结构是“应用背景系统设计测试”信息密度高期刊文章则适合支撑“现状与问题”的论述。不要一上来就啃大论文先搭骨架再填肉。骨架建议分五块引言、国内外研究现状、相关技术分析、本设计的选择与创新点、参考文献。其中“本设计的选择”是很多同学漏掉的也是答辩老师最关心的。操作上我建议你在草稿阶段就建一个对比表一列写文献来源一列写对方用了什么技术一列写局限最后一列写“对本设计的启发”。这个表不一定进论文但能逼着自己把每篇文献和你的技术方案建立关系。比如你要写“为什么用 iOS 原生而不是跨平台框架”就可以从文献里找“跨平台在调用系统定位和通知时性能消耗高”的结论再映射到自己的选型。综述里不需要把每种框架都写一遍只要把文献结论转化为你的选型理由这段综述就有了说服力。2.2 iOS 端技术栈选型UIKit 还是 SwiftUI纯代码还是 Storyboard毕设技术选型不是越新越好而是要在“会讲明白”和“能跑起来”之间取平衡。SwiftUI 在代码量和状态管理上比 UIKit 友好但要注意最低系统版本。如果 Deployment Target 设成 iOS 15 以上SwiftUI 的很多特性都能用如果还要兼容 iOS 13/14一些组件和 Swift 新语法会有限制。我的建议是除非学校实验环境还停留在 iOS 12否则优先选 SwiftUI Swift Concurrency。UIKit 资料多但同样的界面代码量差不多是 SwiftUI 的一倍对写论文来说还要贴更多截图。纯代码和 Storyboard 之争也影响落地。毕设如果用 Storyboard两个人往一个 xib 上拖控件git 冲突能让人崩溃纯代码布局的 diff 清晰也方便在代码里加注释。天气 App 界面不复杂用 SwiftUI 的声明式语法可以一次写完。如果你身边同学都在用 UIKit也不用慌SwiftUI 项目能通过UIViewControllerRepresentable桥接 UIKit 控件这是进阶点毕设一般用不到。创建工程时的关键步骤是在 Xcode 的 New Project 模板里选择 AppInterface 选 SwiftUILanguage 选 SwiftLife Cycle 用 SwiftUI App。生成后你会得到App.swift和ContentView.swift两个文件。工程名不要带空格Bundle Identifier 用域名反写比如com.example.weatherapp。这里有个容易忽略的细节Bundle Identifier 会关联后面的定位权限、配置文件和签名信息一开始想好不要随手写。技术栈对比可以参考下面表格这个表也可以放进文献综述的“相关技术分析”一节方案学习成本代码量适合人群UIKit Storyboard低多想快速搭原型UIKit 纯代码中中熟悉 AutolayoutSwiftUI 纯代码中低少新项目为主2.3 天气数据源选型免费 API 的参数、配额与合规天气数据源是 App 的“燃料”也是文献综述里“国内外研究现状”的天然素材。常见的数据源有 OpenWeatherMap、和风天气、高德开放平台。挑一个适合毕设的我一般按三个标准中文文档是否完整免费档能否满足演示是否支持按城市 ID 或经纬度查询。下面这个表可以先用起来数据源文档语言常用查询参数免费档适合场景OpenWeatherMap英文q / id / lat / lon / appid有限次/分全球城市、海外对比和风天气中文location / key有限次/日国内城市优先高德天气中文city / key / extensions有限次/日与地图生态联动请求参数里最容易翻车的是units和lang。OpenWeatherMap 不传unitsmetric返回的是开尔文温度直接把数值显示出来会得到一个“三百多度”的冷笑话langzh_cn不传description就是英文。和风天气默认公制但location参数可传城市 ID、经纬度或行政区编码三种格式的解析优先级不一样值得在论文里写清楚。还有 API Key 的合规使用。毕设经常把 key 直接写在代码里不违反任何规定但要是把代码传到公开仓库key 很容易被扫描工具拿走。常见做法是把 key 放到单独的Config.plist并加入.gitignore更严谨的做法是在自己的服务器上转发请求客户端只和自己后端通信。不需要写后端也可以至少从WeatherService里把 key 抽离出来不要散落在网络函数里。这些内容都能成为文献综述“安全性设计”的小节素材。3. 把天气数据拉下来网络层与 JSON 解析的最小可跑通实现3.1 建立工程与依赖管理Xcode 项目配置要点创建完工程先检查两件事Deployment Target 和网络层依赖。async/await 需要 iOS 15所以把目标版本设成 iOS 15 或更高。模拟器、真机都能跑不用刻意向下兼容。网络层我不用 Alamofire原因有三个第一毕设论文要解释网络请求怎么封装系统 URLSession 逻辑清晰、源码可见第二天气 App 的请求只有 GET框架的批量处理能力用不上第三第三方库版本升级频繁今天能编译的代码过半年可能因为 API 变动报错。这些理由写进文献综述本身就是一段“技术选型”。依赖管理方面哪怕你看到网上的工程都在用 CocoaPods也不要为了用而用。Swift Package Manager 是 Xcode 原生支持如果一个依赖都不加选型问题最少。你需要用到的三个能力——网络、定位、UI——都是系统库全部零依赖。零依赖的项目在答辩演示时最稳不怕机器上拉不到依赖仓库。文件组织上我建议在 Xcode 里建几个 groupModels放 Codable 结构体Services放网络和定位ViewModels放状态管理Views放 SwiftUI 界面。很多同学把全部代码堆在ContentView.swift做到一半自己都找不到文件更无法向老师展示架构。这里不需要用复杂的文件夹同步直接让文件按目录归类即可。3.2 用 URLSession 和 Codable 请求天气数据核心代码与参数说明一个能跑通的最小闭环只需要一个模型结构体和一个请求函数。先定义模型这里以 OpenWeatherMap 实时天气接口为例struct WeatherResponse: Decodable { let city: String? let main: MainWeather? let weather: [WeatherDetail]? struct MainWeather: Decodable { let temp: Double let feelsLike: Double let humidity: Int } struct WeatherDetail: Decodable { let id: Int let description: String? } }这段结构体对应返回 JSON 里的main和weather数组。注意两个字段名JSON 里温度体感字段是feels_like如果不用解码策略feelsLike直接赋值会失败。常见做法是给模型加CodingKeys或者在JSONDecoder上设置keyDecodingStrategy .convertFromSnakeCase。我推荐用解码策略因为整段 JSON 里几乎所有多词字段都是下划线风格一次配置省掉所有麻烦。接下来是请求函数enum WeatherError: Error { case invalidURL case badResponse case decodeFailed(Error) } struct WeatherService { let apiKey: String func fetchWeather(cityID: String) async throws - WeatherResponse { let urlString https://api.openweathermap.org/data/2.5/weather?id\(cityID)appid\(apiKey)unitsmetriclangzh_cn guard let url URL(string: urlString) else { throw WeatherError.invalidURL } let (data, response) try await URLSession.shared.data(from: url) guard let http response as? HTTPURLResponse, http.statusCode 200 else { throw WeatherError.badResponse } let decoder JSONDecoder() decoder.keyDecodingStrategy .convertFromSnakeCase decoder.dateDecodingStrategy .secondsSince1970 do { return try decoder.decode(WeatherResponse.self, from: data) } catch { throw WeatherError.decodeFailed(error) } } }逻辑说明URLSession.shared.data(from:)是 iOS 15 开始可用的异步版本它返回(Data, URLResponse)。这里先检查状态码再进入解码。keyDecodingStrategy和dateDecodingStrategy是两个很关键的参数前者解决下划线命名后者解决 Unix 时间戳。如果你在接口里收到的是字符串时间就要用自定义日期格式而不是死记这个配置。解码失败时不要把原始错误直接扔给 UI而是统一成WeatherError里的案例让 ViewModel 只感知业务错误。参数说明appid就是你的 API key建议从Config.plist注入unitsmetric让温度变成摄氏度langzh_cn让描述返回中文。id对应城市 ID在 OpenWeatherMap 官网的city.list里可以查到。也可以按latlon查询这样可以直接对接后面的 CoreLocation。两种方式都能用但论文里最好只写一种我自己的习惯是按坐标查省去城市名模糊匹配的问题。3.3 必调参数与错误处理超时、重试、状态码只写一个请求函数还不够实际演示时最容易见到的是“转圈半天然后失败”。这里有一个绕不开的参数超时时间。URLSession.shared的默认超时在弱网下表现并不理想我一般会单独配置let config URLSessionConfiguration.default config.timeoutIntervalForRequest 10 config.timeoutIntervalForResource 20 let session URLSession(configuration: config)timeoutIntervalForRequest控制从请求发出到收到响应的间隔timeoutIntervalForResource控制整个任务生命周期。天气 App 的数据量不高10 秒足够。你把 10 改成 5 反而容易在移动网络下误报改成 30 会让用户一直盯着加载状态。这个数字值得在答辩时说出依据。错误处理方面我见过很多同学一遇到失败就把错误print到控制台界面上一句提示都没有。正确的做法是至少区分网络不可达、超时、服务端错误、数据解析错误。Swift 里用URLError可以判断.timedOut、.notConnectedToInternet等。我的习惯是在请求函数外面包一层重试逻辑专门应对服务端 5xx 或瞬时网络抖动extension URLSession { func dataWithRetry(from url: URL, retries: Int 2) async throws - Data { var attempt 0 while attempt retries { do { let (data, response) try await self.data(from: url) if let http response as? HTTPURLResponse, http.statusCode 500 { attempt 1 if attempt retries { throw WeatherError.badResponse } try await Task.sleep(nanoseconds: 1_000_000_000) continue } return data } catch { attempt 1 if attempt retries { throw error } try await Task.sleep(nanoseconds: 1_000_000_000) } } throw WeatherError.badResponse } }这段扩展的逻辑很简单遇到 5xx 状态码或者抛错每等 1 秒重试最多重试两次。这里的 1 秒是经验值太短会让服务端更忙太长会让用户等待。重试不要写成无限循环答辩时一旦被问“如果一直失败怎么办”你就能回答超过次数抛出错误并展示失败页面。真正的生产系统还会考虑指数退避但对于毕设固定间隔足够把重试意图写清楚即可。状态码也要分级处理401 说明 key 无效404 说明城市 ID 不存在429 说明免费配额用完了。这几类错误应该映射到不同的用户提示比如“天气服务配置异常”和“当前查询量已达上限”而不是统一显示“网络错误”。把状态码映射逻辑放在WeatherService里而不是放在 SwiftUI 视图里这样论文中的“服务层”设计才立得住。4. 让界面真正“活”起来定位、状态管理与 UI 更新4.1 申请定位权限并获取坐标CoreLocation 的配置与代码天气 App 的“智能化”体现在自动定位。但定位不是技术玄学它有明确的权限流程。要在Info.plist里加NSLocationWhenInUseUsageDescription描述文案比如“用于获取当前城市并展示当地天气”。不写描述真机运行时系统会直接拒绝请求。这种表现不是崩溃而是权限配置缺失属于最容易被忽略的硬伤。使用 CoreLocation 的典型封装如下import CoreLocation import Combine final class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate { private let manager CLLocationManager() Published var location: CLLocation? override init() { super.init() manager.delegate self manager.desiredAccuracy kCLLocationAccuracyKilometer } func requestPermission() { manager.requestWhenInUseAuthorization() } func startUpdate() { manager.startUpdatingLocation() } func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { if manager.authorizationStatus .authorizedWhenInUse || manager.authorizationStatus .authorizedAlways { startUpdate() } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { location locations.last manager.stopUpdatingLocation() } }这里有两个参数值得讲。desiredAccuracy设为kCLLocationAccuracyKilometer因为天气只需要城市粒度不需要精确到米如果用kCLLocationAccuracyBest定位芯片会高频搜索电池消耗明显增加。stopUpdatingLocation()是一个容易被忽略的关键动作拿到一次位置就停掉避免 App 在后台持续上报。如果你希望每次打开刷新天气可以在scenePhase变化时再次调用startUpdate()而不是常开。requestWhenInUseAuthorization与requestAlwaysAuthorization的选择天气 App 只需要前台定位用WhenInUse就够了。强行申请后台定位会增加审核风险用户也很容易拒绝。调试时如果模拟器位置没设置App 会一直拿不到定位这会直接表现为“无法获取定位”。设置路径是 Simulator 菜单的 Features - Location - City Run / Custom Location。这一条也可以写进文献综述的“权限设计”小节。4.2 用 ViewState 驱动界面刷新MVVM 状态管理的落地写法很多同学写 SwiftUI 喜欢把网络请求直接写在Button的 action 里再用一个State var weather: WeatherResponse?存结果。这样做的问题是“加载中”状态无处安放你要么再加一个State var isLoading要么用nil表示加载中结果出错又要再开一个State var errorMessage。三个状态变量互相组合很容易出现“既在加载又显示错误”的界面。我推荐用一个枚举状态机把三种状态折叠成一个值enum ViewStateT { case loading case success(T) case failure(String) }ViewState是带关联值的枚举Swift 的switch会强制穷尽所有情况少处理一种就编译不过。用这个状态驱动 ViewModelMainActor final class WeatherViewModel: ObservableObject { Published var state: ViewStateWeatherResponse .loading private let service: WeatherService private let locationManager: LocationManager init(service: WeatherService, locationManager: LocationManager) { self.service service self.locationManager locationManager } func load() async { state .loading guard let location locationManager.location else { state .failure(无法获取定位请检查权限设置) return } do { let weather try await service.fetchWeather( lat: location.coordinate.latitude, lon: location.coordinate.longitude ) state .success(weather) } catch { state .failure(天气加载失败请稍后重试) } } }注意这里的MainActor。因为async函数会在后台线程执行如果函数内部直接更新Published属性而外部没有切换到主线程UI 刷新就会在错误线程进行。Swift 5.5 之后给 ViewModel 加MainActor注解所有属性访问默认在主 actor 上执行省去DispatchQueue.main.async的样板代码。这是 Swift Concurrency 里最容易让人“看得懂但写不出一致”的地方也是答辩时能体现你用过新特性的细节。有一个边界值得说明这里的fetchWeather(lat:lon:)与前面写的fetchWeather(cityID:)参数不同。如果接口不支持按坐标你就在LocationManager里用geocoder.reverseGeocodeLocation把坐标转成城市名再把城市名传给原函数。但这会引入额外失败点。我更推荐直接选一个支持latlon参数的天气接口让“定位到天气”只经过一次网络请求。这样的数据流在论文里也更容易画清楚。4.3 用 SwiftUI 展示天气数据最小界面和版本边界SwiftUI 的界面代码可以保持很薄struct WeatherView: View { ObservedObject var viewModel: WeatherViewModel var body: some View { Group { switch viewModel.state { case .loading: ProgressView(正在加载天气) case .failure(let message): VStack(spacing: 12) { Text(加载失败) .font(.headline) Text(message) .font(.subheadline) .foregroundColor(.secondary) } case .success(let weather): VStack(spacing: 12) { Text(weather.city ?? 当前城市) .font(.largeTitle) if let main weather.main { Text(\(Int(main.temp))°C) .font(.system(size: 72, weight: .thin)) } if let detail weather.weather?.first { Text(detail.description ?? --) .font(.title3) } } } } .task { await viewModel.load() } } }Group在这里不会改变布局只是让switch的每个分支都能返回同一类型。.task是 SwiftUI 的异步生命周期入口等效于onAppear里启动一个Task但会在视图消失时自动取消。把await viewModel.load()放进.task能避免在onAppear中手动管理 Task 取消的问题。版本边界ProgressView需要 iOS 14Color.secondary没问题。如果你把 Deployment Target 降到 iOS 13这组界面也能跑但.task不可用要退回onAppear。iOS 17 的ContentUnavailableView在失败场景更好看但对毕设来说不是必要的。我的原则是让代码在尽可能低的系统版本上编译通过减少交代码后因为“跑不了”产生的沟通成本。如果你用的是 UIKit也可以在UIViewController里用UIActivityIndicatorView展示 loading再用NSDiffableDataSourceSnapshot更新 UITableView但那样代码量会多很多。这不是说 UIKit 不好而是同样的需求用 SwiftUI 的声明式写法更容易在正文里截短代码。文献综述里写“SwiftUI 数据驱动 UI”恰好能与Published state形成闭环。5. 无论代码还是论文都可能翻车天气 App 毕设的 5 个避坑记录5.1 模拟器定位失效现象、原因、解决现象在模拟器上点击获取天气界面一直显示“无法获取定位”但真机一切正常。原因模拟器默认不会自动按宿主机位置上报需要手动指定一个模拟位置。很多人把定位失败当成代码问题浪费大量时间。解决在 Simulator 的菜单栏选 Features - Location - City Run 或 Custom Location给模拟器设定一个城市再重新运行 App。如果你之前调过定位且点了“拒绝”记得先在模拟器设置里重置隐私权限否则系统记住“拒绝请求”后续代码改得再多也不会再弹窗。另一个常见原因是Info.plist权限描述写得太晚。首次请求定位时如果权限描述缺失模拟器和真机都会直接触发异常。所以写定位代码之前先把NSLocationWhenInUseUsageDescription加上。这个坑不涉及复杂逻辑但足够让第一次做 iOS 项目的人卡一整天。5.2 HTTP 请求被 ATS 拦截现象、原因、解决现象控制台打印 “App Transport Security has blocked a cleartext HTTP resource load”请求始终失败。原因iOS 默认只允许 HTTPS 传输不允许明文 HTTP 资源。很多毕设为了省事在本地起了个 HTTP 后端或者拉一个 HTTP 的图片地址就会撞上这个机制。解决生产环境必须用 HTTPS 接口。只有开发阶段你才可以在 Info.plist 里针对localhost或内网域名加例外例如下面这样keyNSAppTransportSecurity/key dict keyNSExceptionDomains/key dict keylocalhost/key dict keyNSExceptionAllowsInsecureHTTPLoads/key true/ /dict /dict /dict注意这里不要顺手把NSAllowsArbitraryLoads设为true。全局放开明文传输会让 App 在真实网络中降级也是审核常见的拒绝理由。如果你的天气 API 本身就是 HTTPS完全不需要配置这一项只在确认需要连内网测试服务时才加例外。这条经验能帮你把“调试时能跑”和“换台电脑就不能跑”的问题消灭在源头。5.3 城市名查不到天气现象、原因、解决现象请求qbeijing能返回请求q北京却返回 404或者同一城市名返回了另一个国家的同名城市。原因OpenWeatherMap 等国际接口支持的城市名是英文或拼音对中文并不友好同名城市在不同国家都存在时文本查询会产生歧义。解决不要用城市名作为查询主键。优先使用城市 ID 或经纬度坐标。城市 ID 在数据源官网的静态列表里提供经纬度可以直接复用定位模块。如果你的代码是从网上的旧博客抄的q城市名这一点最容易成为答辩时被追问的“设计缺陷”。在文献综述里可以写一句本设计采用坐标查询以避免城市名歧义。这就是一个很自然的“创新点”。更进一步你可以在模型里维护一个“城市 ID 映射表”把常用城市名转成 ID作为离线兜底。这个映射表不需要完整覆盖演示用的几个城市就够。用它也能解释清楚“为什么我不用第三方地图 SDK”。5.4 文献综述被批“没有观点”现象、原因、解决现象答辩老师说“你的文献综述只是把别人的摘要串了一遍没有回答你自己的问题”。原因综述没有按主题组织也没有把文献结论投影到你的技术栈上。解决给每个主题写一个对比草表三列——别人怎么做、问题是什么、我怎么做。例如“基于 Android 的天气 App 采用 Java Volley问题Android 碎片化适配成本高本设计采用 iOS 原生 SwiftUI统一渲染管线”。把这类对比写进正文之后综述就不再是翻译汇总而是决策依据。具体到写作节奏先写完“技术选型”再回头补综述比先综述后设计顺畅得多。因为你在写代码的过程里已经做完了技术决策此时再去读文献能自然地说出“哪篇论文启发了我的接口设计”“哪个方案我认为不适用于校园场景”。这比对着空白文档硬写更有条理。5.5 正式提交时的 iOS 上架流程与图标文件现象、原因、解决现象用 Xcode 提交到 App Store Connect构建版本在“缺少功能”里提示没有 1024 尺寸的图标或收到权限描述不清晰的警告。原因iOS 上架流程要求 AppIcon 完整覆盖所有尺寸并且使用用途描述必须和代码行为一致。你只在模拟器跑不需要上架但很多学校要求展示安装包或演示发布能力。解决在 Assets.xcassets 的 AppIcon 里放置 1024 x 1024 的 iOS 图标文件不允许透明通道在 TARGETS - Info 中确认定位权限描述和请求时机。如果只是毕设演示不建议走完整上架流程因为开发者账号和审核周期会拖慢进度但资料可以先准备。这个坑属于“最后一步翻车”往往在交材料前才暴露血泪经验是提前两天检查图标和权限文案不要压在最后一天。6. 让毕设从“能跑”到“能答辩”离线验证与一个实用技巧6.1 用本地 JSON 文件做离线联调天气 App 最怕演示时网络掉链子。即使你的接口写得没问题学校会议室网络也可能不配合。我一般会在工程里放一份样本 JSON再用一个极简 URLProtocol 把网络请求“换掉”。这样即使完全离线界面也能展示成功态。final class StubURLProtocol: URLProtocol { static var stubData {main:{temp:23.5,feels_like:21.0,humidity:60},weather:[{id:800,description:晴}]} .data(using: .utf8) override class func canInit(with request: URLRequest) - Bool { true } override class func canonicalRequest(for request: URLRequest) - URLRequest { request } override func startLoading() { let response HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: [Content-Type: application/json])! client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: StubURLProtocol.stubData!) client?.urlProtocolDidFinishLoading(self) } override func stopLoading() {} }在测试环境里把这个协议插入到URLSessionConfiguration的protocolClasses数组首位原来的WeatherService不需要改。这个技巧的价值不只是演示保险它还能让你稳定复现“成功响应”的 UI不用反复消耗 API 配额。缺点是样本 JSON 很容易写旧和真实字段不一致所以离线联调通过后一定要在真机上再比对一次真实返回。6.2 验证方法解析测试大于集成测试毕设的验证部分重点不是测试覆盖率而是“证明你的设计成立”。我会优先写两个解析测试一个用样本 JSON 测WeatherResponse能否正确解码一个测ViewState从 loading 到 success 的状态迁移。这两个测试对应论文里的“模块测试”和“逻辑验证”加起来代码量不超过 50 行但答辩时有东西可说。真机调试这一步别跳过。模拟器里的定位、网络、温度显示都是虚拟环境真机上你才能发现定位弹窗时机、状态栏高度、后台切换回来刷新时机这些“体验问题”。真机调试前记得在 iPhone 的设置里开启“开发者模式”这是 iOS 16 之后第一次连接 Xcode 的默认前提。我第一次做天气 App 时把 Service 和 ViewModel 写成了一个类切换后台再回到前台时 UI 卡在旧状态演示时直接翻车。后来把所有网络逻辑收进 Service定位收进 LocationManagerViewModel 只负责状态流转才真正体会到“分层不是给别人看的是给自己留的后路”。希望这篇从文献综述到代码的实现思路能让你少走一点我走过的弯路。先跑通最小闭环再回来补理论最后用离线样本兜底你的天气 App 毕设就能同时搞定代码和论文两条线。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑