Lessons learned with Swift and iOS development

Ellie Huxtable

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 了!

如果你骑摩托车,并决定试用一下,我很想知道你的想法——所以请随时联系我!

原文由 Ellie Huxtable 发布

本文章由 openai/gpt-5.6-luna 进行翻译