Swift 和 iOS 开发中的经验教训
首先我想说明,我绝对算不上专家。虽然我已经编程很长时间了,但我刚接触 Swift 和 iOS,也从没开发过移动应用 :) 这些就是我在构建 Pillion 的第一个 iOS 版本时遇到并学到的所有东西!
Pillion 是一款帮助寻找摩托车停车位的应用——我在全球各地列出了大约 10,000 个停车地点,其中一些来自 OpenStreetMaps,一些来自在线数据集,其余来自用户。除了停车功能,它还提供骑行追踪功能
其中有很多内容是在我还没完全搞清楚各种问题时发现的,所以有不少地方我当时并没有采用“正确”的做法。后来了解到更好的方式后,我把两种方法都列了出来。
如果这篇文章反响不错,我将来可能会针对每个部分分别写更详细的文章(尤其是 MapBox)。
如果你发现任何错误,请告诉我 😊
SwiftUI
我直接选择了 SwiftUI,也就是 Apple 随 iOS 13 发布的新 UI 系统。需要注意的是,它只支持运行 iOS >= 13 的设备——不过这意味着 iPhone 6S 及之后的所有设备都支持。
由于我已经使用 React 多年,SwiftUI 用起来感觉非常舒服。虽然它有一些差异,但组件、子组件和状态等概念都很自然。我其实更喜欢它的状态绑定方式,而不是 React 处理状态的方式,而且代码看起来也简洁不少。
我从没尝试过 UIKit,所以没有太多可比较的东西,但就我对 SwiftUI 的了解来看,它干净得多。
我在使用 SwiftUI 时遇到过一些问题,这些问题似乎主要与它还不够成熟有关——下文会讲到。
令人恼火的是,我在 GitHub 上找到的大多数好用的 UI 组件都只支持 UIKit。也许我之后会尝试把它们封装起来,但 SwiftUI 的生态似乎有些稀疏——这很好地反映了它还不够成熟这一点。需要记住这一点。
调试
XCode 调试器给了我一个惊喜!虽然我习惯了使用 Python 的 ipdb 会话,但考虑到代码是在我的手机上运行的,而且还是编译后的代码,我没想到它能如此灵活。
我发现调试器可以很轻松地检查值、计算 Swift 表达式,完成所有我想做的事情!制表符补全也非常好用。工具链万岁 :)
它偶尔会有点慢,不过我想这也在意料之中。
UI 线程相关
我的应用有很大一部分工作本质上是向我的 API 发起请求,然后渲染结果。我发现,尝试从后台线程(background thread)更新 UI(例如在 HTTP 响应的异步处理程序中更新)会导致错误。相反,应该使用下面这种模式:
DispatchQueue.main.async {
// do ui
}这样就能非常轻松地避免阻塞 UI 线程 😁
其实这是我刚开始使用 Swift 不久时读到的内容。后来我了解到,在可以使用的地方,使用带有一些 Published 属性的 ObservableObjects,才是获取和更新数据更推荐的方式!不过我还是把这部分留在这里,以防有人需要。我的项目对 HTTP 请求的处理方式目前仍不太令我满意,所以将来可能会重新处理这部分。
隐藏键盘
这只是个小问题,但我希望能在某些交互时隐藏键盘。比如,当搜索栏处于聚焦状态,而你触摸列表中的某个项目时,就隐藏键盘。
extension UIApplication {
func hideKeyboard() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}UIApplication.shared.hideKeyboard()Just
在 Swift 中发起 HTTP 请求并不算太糟,但我习惯了 Python 中的 requests,所以我找了找有没有类似的东西。
Just 看起来正合适!它好用、小巧、快速,而且可以让你这样写:
Just.get("https://example.com") { r in
print(r.text)
}……非常棒!
它还允许你创建一个预先设置好请求头的会话,这对于需要授权的 HTTP 请求很有用
Mapbox
在这个项目的大部分开发过程中,我一直在使用 MapBox Maps SDK for iOS。在 JavaScript 中使用 Mapbox 是一种享受,但坦率地说,iOS 这边就有些欠缺了。自定义一些东西时可能会很麻烦,文档也不是最完善的。我经常发现有多种方式可以完成同一件事,却几乎没有解释为什么。看来 Android 在这方面可能会好一些!
我不是说它很差,只是与 JavaScript 方案相比,它感觉有点未完成或开发不足——根据我对 Android 版本的了解,与 Android 版本相比也是如此。等我完成 Android 应用后,会做一次比较。
还有一点是,部分文档更偏向 UIKit——毕竟 SwiftUI 还很新,这也情有可原!只是需要记住这一点。不过,也有一些文档会教你如何为 SwiftUI 封装地图
Swift 类型检查
我之前使用 Rust,因此 Swift typechecker(类型检查器) 给我的感觉有点……不够完善。它偶尔会完全无法推断出一个看似显而易见的类型,或者花费太长时间,然后要求开发者把代码拆得更细一些。这不算什么大问题,但确实有点烦人。显然它一直在不断改进!
有些情况下,它会在某个位置标出错误,但那个位置实际上没有任何问题。删除那段代码后,才会显示真正的底层问题。我认为这更多是 SwiftUI DSL(领域特定语言) 的问题。当 DSL 内部出现类型错误,而它又无法推断出类型时,整个东西就会有点崩溃。我现在找不到一个好的例子,不过找到后会更新这篇文章。
升级到 Catalina 和最新版本的 XCode 后,我遇到了一堆以前从未出现过的奇怪“ambiguous member”错误。Swift 似乎倾向于给出误导性的错误信息,这……很奇怪。我从没用过其他也会这样做的现代语言,尤其是 Swift 背后还有这么大的公司支持。
我发现自己最终越来越擅长编写 Swift 能够理解的代码,尽管据我所知,我之前写的代码在语义上也是正确的。
SwiftFormat
我习惯在其他项目中使用格式化工具,所以也在这里找了一个。SwiftFormat 用起来似乎不错
SwiftUI DSL 的问题
在 SwiftUI 中,View 的主体是用一种小型的 DSL 来声明的
var body: some View {
ComponentOne {
AnotherOne {
// so much nesting :O
}
}
}……这本身没什么问题。
但是,当你想进行条件渲染时,这个 DSL 似乎还不太完善。
if thing.ouch {
RenderMe(thing.ouch.thing)
}不过,在撰写本文时,诸如 if let 这样的写法还不能使用
if let o = thing.ouch {
RenderMe(o.thing)
}虽然不是必需的,但有这个功能会很好!
这个 DSL 还有其他一些零零碎碎的问题需要解决,不过 SwiftUI 还很新,甚至仍处于 beta 阶段,所以我对此并不意外。除去这些问题,它用起来非常愉快。能正常工作时,效果真的很棒!只是有时它不工作 ;)
Auth0
我目前使用的是 Auth0 lock,并通过 Google + Apple oauth 登录。不过我现在已经改用自己的方案了,但这部分内容仍然具有参考价值,所以我保留了下来。
Apple 的登录功能其实非常酷!用户可以选择使用 Apple 的转发地址来隐藏自己的电子邮件地址,还可以通过 FaceID 登录。
Lock 并不是为 SwiftUI 设置的,但封装起来相当容易:
struct Auth0Lock: UIViewControllerRepresentable {
typealias UIViewControllerType = LockViewController
var lock: Lock = Lock.passwordless().withOptions {
$0.passwordlessMethod = .magicLink
}
.onAuth { credentials in
print(credentials)
// save access token and use it to do stuff :)
}
.onError{ error in
print(error)
// you should probably do something other than just print errors
}
func makeUIViewController(context: UIViewControllerRepresentableContext<Auth0Lock>) -> LockViewController {
return lock.controller
}
func updateUIViewController(_ uiViewController: LockViewController, context: UIViewControllerRepresentableContext<Auth0Lock>) {
// there isn't really much updating I need to do atm
}
}Auth0 自带一个凭据管理器,可以处理刷新令牌、请求新的访问令牌、保存和验证令牌等事情。所以你不需要投入太多精力,就能让用户保持登录状态!
稍微解释一下我为什么弃用了 Auth0我希望在身份验证的处理方式上拥有更大的灵活性,后来又把后端重写成了 Django 应用——因此直接整合 Django 已有的功能对我来说非常容易。我改用 Django,是为了更快地构建产品,而不是把时间花在思考有趣的架构问题上
Apple 开发者
为我的公司(Pillion Software Ltd)注册 Apple Developer 账户大概花了一两周时间——填写在线表格很快,但审批花了一些时间,而且他们还必须给我打电话。不过之后一切都很顺利!
我尝试使用 Fastlane 截取屏幕截图,但发现模拟器和测试运行得不太好(Mapbox 在 UI 测试中崩溃),所以唯一可行的方法就是手动截取几张。效果不太好,我希望能解决这个问题。我最终总会抽时间处理的。我的笔记本电脑运行速度非常慢,而且有点老(主要是 RAM 不足),这也让模拟器的表现糟糕得不行。
第一次提交应用到 App Store 后,我被告知应用的功能还不够。真是痛苦。经过一些研究后,我发现,对于那些本可以做成 Web 应用的东西,Apple 似乎不太愿意批准。因此,我只好把路线图中的一些功能提前了。
我的应用确实有一个版本顺利完成了整个审核流程(从我按下“submit”到应用上线),总共大约只用了 7 分钟,这让我非常惊讶。
结论
现在回头看,我想我可能应该在直接使用旧 Web 版本相同的技术之前,先研究一下其他方案。也许 MapKit 完全可以满足需求,这样我就不会使用 MapBox 了。
不过,我确实从这个过程中学到了很多,所以我不认为自己会改变太多东西。很快我就要开始为 Android 构建同样的应用了(而且我也还不太懂 Android 开发 ;P),所以我们到时就知道结果了!敬请期待一篇类似的文章。
总之,Pillion 现在已经上架 App Store 了!
如果你骑摩托车,并决定试用一下,我很想知道你的想法——所以请随时联系我!
随机一篇博客