iOS开发入门:那些年踩过的Swift坑,今天我帮你绕过

掘金独行侠
2025-06-16 14:19
阅读 560

介绍背景:从Android转战iOS的“菜鸟”之路

介绍背景:从Android转战iOS的“菜鸟”之路

其实一开始,我对iOS开发是有点抗拒的。作为一个Android开发者出身的人,在Java/Kotlin上已经滚瓜烂熟了,看到Objective-C的时候整个人都不好了。后来苹果把Swift推出来之后,情况才慢慢好起来。直到公司要组建一个跨端项目组,需要双端并行开发,作为原生移动组里唯一没有iOS经验的老兵,我被“光荣地”扔进了这个坑——开始了我的Swift学习和iOS实战旅程。

这一趟走下来,说不难是假的。但真正难的不是语法本身,而是环境搭建、调试方式、系统API的设计理念以及各种细节上的“坑”。所以这篇文章,我想结合我在实际项目中踩过的几个真实大坑,来讲讲Swift的基础知识到底应该怎么学,以及新手在入门阶段最容易掉进去哪些陷阱。


第一次实战:用Swift写一个“天气+日程”的小App

第一次实战:用Swift写一个“天气+日程”的小App

项目背景与挑战

我们的项目是一个面向内部员工的日程管理工具,需要集成每日天气信息,方便提醒用户外出穿衣、是否带伞等。因为涉及位置获取、网络请求、数据展示等多个模块,对于刚接触Swift的我来说是个不错的小试牛刀机会。

项目初期的目标比较简单:

  • 使用定位服务获取当前位置
  • 调用公开天气API(如OpenWeatherMap)
  • 展示今日天气+未来三日预报
  • 展示当天用户的日程安排列表

听起来挺简单吧?可真做起来才发现,Swift和iOS平台的很多机制跟安卓完全是两码事。


解决方案思路:从零开始搭一个骨架结构

先说整体架构选择。我们采用了MVC + Alamofire + Kingfisher这样的基础组合(虽然现在很多人推荐MVVM或者Combine,但在那个时间点,考虑到学习成本,还是选了个熟悉的结构)。

整个项目的目录结构大致如下:

/Controllers
    - WeatherViewController.swift
    - ScheduleViewController.swift
/Models
    - WeatherModel.swift
    - ScheduleModel.swift
/Services
    - WeatherService.swift
    - ScheduleService.swift
/Resources
    - Images.xcassets
    - Info.plist配置项

每个视图控制器负责自己的UI逻辑,调用对应的服务层来获取数据,模型类用于解析JSON响应。这看起来很常规,对吧?但就是在这个看似简单的结构下,我碰到了第一个大问题。


坑点一:Swift语法灵活但容易“掉沟”

坑点一:Swift语法灵活但容易“掉沟”

情况还原

我第一次写的fetchWeather函数大概是长这样的:

func fetchWeather(location: Location?, completion: (Result<WeatherModel, Error>) -> Void) {
    guard let lat = location?.lat,
          let lon = location?.lon else {
        completion(.failure(NSError(domain: "Location is invalid", code: -1, userInfo: nil)))
        return
    }

    AF.request("https://api.openweathermap.org/data/2.5/weather?lat=$lat)&lon=$(lon)&appid=YOUR_API_KEY")
       .responseDecodable(of: WeatherModel.self) { response in
           switch response.result {
               case .success(let model):
                   completion(.success(model))
               case .failure(let error):
                   completion(.failure(error))
           }
    }
}

这段代码看起来没什么问题,但实际上跑起来经常报错或者拿不到数据,尤其是AF.request()部分的拼接方式让我debug了整整半天。

原因分析

  1. 字符串插值错误$(lat) 这种写法完全就是 Swift 的语法错误!正确的应该是 $lat),少一个大括号,但编译器居然没给我提示。
  2. URL拼接方式不安全:直接拼URL是非常非常不推荐的做法,应该使用URLComponents来构造。
  3. Optional处理不够完善:虽然用了guard let判断,但如果location传进来的是nil,函数就直接崩溃了。

正确做法建议

后来我改成了这样:

func fetchWeather(location: Location?, completion: @escaping (Result<WeatherModel, Error>) -> Void) {
    guard let location = location else {
        completion(.failure(APIError.missingLocation))
        return
    }

    var components = URLComponents(string: "https://api.openweathermap.org/data/2.5/weather")!
    components.queryItems = [
        URLQueryItem(name: "lat", value: String(location.lat)),
        URLQueryItem(name: "lon", value: String(location.lon)),
        URLQueryItem(name: "appid", value: Constants.weatherApiKey)
    ]

    guard let url = components.url else {
        completion(.failure(APIError.invalidUrl))
        return
    }

    AF.request(url)
      .validate(statusCode: 200..<300)
      .responseDecodable(of: WeatherModel.self) { response in
          switch response.result {
              case .success(let model):
                  completion(.success(model))
              case .failure(let error):
                  completion(.failure(error))
          }
      }
}

这个版本增加了:

  • 对url的严谨构建
  • 更规范的错误类型定义(比如APIError枚举)
  • @escaping修饰符显式声明逃逸闭包
  • 使用了Alamofire内置的状态码校验功能

而且最重要的是——编译能过了 😂


坑点二:SwiftUI初体验:界面怎么这么难调?

坑点二:SwiftUI初体验:界面怎么这么难调?

小插曲:领导想尝试SwiftUI试试看

项目中期,老板觉得Apple官方一直在大力推SwiftUI,不如我们试试能不能重构一部分看看效果。于是我就临时决定给天气页换一个UI框架。

当时的想法是:“SwiftUI应该会更方便吧?毕竟声明式UI嘛。”然而……

struct WeatherView: View {
    @State private var weather: WeatherModel?

    var body: some View {
        if let weather = weather {
            VStack(alignment: .leading) {
                Text(weather.name)
                    .font(.title)
                
                Text("Temperature: $weather.main.temp)°C")
                    .font(.body)
            }
            .padding()
        } else {
            Text("Loading...")
        }
    }

    init() {
        fetchWeather()
    }

    private func fetchWeather() {
        WeatherService.shared.fetchWeather(location: UserLocationManager.shared.currentLocation) { result in
            switch result {
                case .success(let data):
                    DispatchQueue.main.async {
                        self.weather = data
                    }
                case .failure(let error):
                    print(error.localizedDescription)
            }
        }
    }
}

移动端调试工具-1

上面这个例子看着好像没问题,结果运行后根本不出数据!

定位原因

原来,SwiftUI 的生命周期和 UIKit 不一样。在 SwiftUI 中,不能像 UIView 那样随便放异步操作到init里。正确的做法应该是用.onAppear()来做初始加载。

此外还有一个致命的问题是:

  • fetchWeather()里更新self.weather是异步的,但SwiftUI要求所有对@State变量的操作必须在主线程进行。

虽然我知道要用DispatchQueue.main.async来保证主线程执行更新,但由于SwiftUI的自动渲染机制不同,有时候还是会遇到刷新失败的情况。

最终解决

struct WeatherView: View {
    @State private var weather: WeatherModel?
    @State private var isLoading = true

    var body: some View {
        Group {
            if isLoading {
                ProgressView("Loading...")
            } else if let weather = weather {
                VStack(alignment: .leading) {
                    Text(weather.name)
                        .font(.title)

                    Text("Temperature: $weather.main.temp)°C")
                        .font(.body)
                }
                .padding()
            } else {
                Text("Failed to load weather.")
            }
        }
        .onAppear {
            fetchWeather()
        }
    }

    private func fetchWeather() {
        isLoading = true
        WeatherService.shared.fetchWeather(location: UserLocationManager.shared.currentLocation) { result in
            DispatchQueue.main.async {
                switch result {
                    case .success(let data):
                        self.weather = data
                    case .failure(_):
                        self.weather = nil
                    default:
                        break
                }
                self.isLoading = false
            }
        }
    }
}

这里主要做了几个优化:

  • 增加了明确的加载状态指示器(ProgressView)
  • 所有数据更新都包裹在.onAppear{}
  • 显式控制isLoading状态避免空页面闪烁

通过这次重构,我也意识到SwiftUI虽然强大,但它不是拿来替代UIKit的万能钥匙。尤其是一些复杂的交互场景或性能敏感的部分,UIKit依然不可替代。


经历分享:适配&性能优化才是大头

随着项目逐渐成型,我们在准备上线前遇到了两个关键问题:

1. iPhone SE 和 iPad 上的UI适配问题

我们在iPhone XS Max上测试一切正常,但部署到老款SE时发现按钮变得奇大无比,布局混乱。iPad上更糟——字体竟然比手机上还小,文字显示模糊。

后来才知道这是Auto Layout没设置好的问题。解决方法包括:

  • 设置按钮高度为比例值而不是固定值
  • 使用动态字体大小,例如.font(.body)而非.font(Font.system(size: 16))
  • 引入ContentSizeCategory监听变化
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    
    if traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
        // 调整文字样式以适应更大字号
    }
}

当然,在SwiftUI里可以更优雅地使用.environment(\.sizeCategory, .accessibilityMedium)之类的API,不过这些都需要提前规划好响应式设计的策略。

2. 性能优化:冷启动慢

最初App打开首页要花个2~3秒才能看到内容,特别尴尬。排查发现:

  • 启动时一次性加载了太多资源(天气+日程+用户偏好设置)
  • 图片解压阻塞了主线程
  • JSON解析嵌套太深导致CPU占用高

优化手段:

  • 拆分初始化流程,优先加载必要数据
  • 使用GCD并发队列预加载非核心数据
  • 把图片加载封装进Kingfisher缓存机制,避免重复下载
  • 将深度JSON模型拆成多个轻量结构体,提升解析速度

最终冷启动时间降到700ms左右,用户体验明显变流畅。


发布经历:上架App Store也是个技术活儿

虽然写代码只是项目的一部分,但真正的“交付”是在App Store上发布成功。

我们遇到的主要问题包括:

  • App权限描述文案缺失(Privacy —— 如需访问定位、相册等,Info.plist中必须写清楚用途说明)
  • 构建归档文件时报code signing错误(证书权限或Provisioning Profile配置不对)
  • TestFlight灰度测试提交被拒,原因是截图不符合规定尺寸(必须是真实设备截图)

Tips:

  • 提前申请苹果开发者账号,并熟悉Certificates, Identifiers & Profiles的操作流程
  • 使用Xcode的"Archive"功能打包之前确保勾选了正确的Build Configuration
  • 提交审核前至少跑一遍自动化UITest,避免低级错误

效果总结:三个月的收获远超预期

最终这个项目我们用了大概三个月时间完成,其中Swift占了很大一块。回头看我觉得自己在以下几个方面进步最大:

  • 语言层面:掌握了Swift的基本语法、可选项(Optional)、闭包、泛型、协议扩展等高频特性
  • 工程实践:熟悉了Swift Package Manager、CocoaPods依赖管理、Xcode调试技巧等
  • 开发思维:从命令式编程转向更偏向声明式的SwiftUI风格,同时也能根据场景切换到传统UIKit
  • 跨端视角:对比Android和iOS之间的差异,更深入理解移动开发的本质和共通性

项目上线后收到了不少积极反馈,特别是界面简洁性和天气数据的准确率得到了好评。


给新手的一些建议

跨平台开发对比-2

如果你也在考虑入门Swift,以下是我踩完坑后的肺腑之言:

  1. 别怕语法不同,关键是逻辑一致 Swift语法可能和你熟悉的语言不一样,但这其实是锻炼你的抽象表达能力的好机会。

  2. 多查文档,少抄代码 Apple官方文档是最权威、最全面的资源。虽然中文资料相对少一点,但英文文档质量很高。

  3. 别迷信SwiftUI,该用UIKit时果断切换 SwiftUI确实爽,但成熟项目中很多地方用UIKit反而更可控。两者并不互斥,混合使用完全没有问题。

  4. 多动手实践,别光看教程 我见过很多同学看完视频觉得自己懂了,可一旦让他自己搭个网络请求就开始卡壳。一定要写

  5. 养成良好的代码习惯 Swift支持强大的类型推导、函数式语法等,但写出来的代码要清晰易读,不要为了炫技而堆砌语法糖。


结语:Swift值得每一个开发者认真对待

现在的iOS生态越来越开放,Swift也已经开源多年。更重要的是,Swift不仅可以在iPhone/iPad上使用,还可以写macOS应用、服务端(Vapor)、甚至Web前端(TCA+SSE),未来的可能性很大。

无论你是刚毕业的学生、转岗的程序员,还是想拓宽技术栈的资深开发者,Swift都是你不可错过的一门语言。希望我这篇踩坑笔记,能让你少走一些弯路,早点进入Swift的“快乐编程世界”。

如果你有任何具体的技术问题,欢迎留言交流。我们一起成长,一起踩更多的坑 😄

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝