本文作者:Ida,对移动端开发拥有深厚的技术积累。近年来,积极探索如何利用 AI 技术提升日常工作效率,并尝试将生成式 AI 应用于研发流程优化。曾参与多个复杂项目的开发与维护,擅长将创新技术融入实际场景,提升用户体验。热爱分享技术心得,致力于让更多人了解并掌握前沿科技。
在 TRAE 中国版正式发布之后,我便有意识地用它来解决日常开发中遇到的各种问题。从一开始谨慎 check 每一条变更再决定是否接受,到现在点击全部接受的概率越来越高,TRAE 正在变成我写代码的主力 IDE。
希望本文能给从事客户端开发的同学在 TRAE 的使用上提供一些启发和参考,如果大家有好的实践探索也欢迎评论交流。
不知道大家用 TRAE 开发客户端功能时一般都是把哪个文件夹丢给 TRAE 呢?
开发业务需求时可以用 TRAE 直接打开工程中源码所在的文件夹,这样基本用到的文件都能在 TRAE 里索引到,也避免了整个工程下其他文件的干扰。
如果是明确针对某个文件夹下的代码分析或修改,有时也会直接用 TRAE 打开该特定文件夹,以便它可以快速建立代码索引,精确地去分析上下文。
但也要注意,TRAE 的历史会话记录是以文件夹维度来存储的,一旦文件夹被删除,之前在该文件夹中发生的历史会话也就全部丢失了!
关于导出历史会话记录,可以用魔法打败魔法,直接让 AI 把会话内容整理到文件中,一句下面这样的提示词即可:
将上面的对话历史内容全部整理到该文件中,包括给出的代码片段,不要遗漏内容,优化输出的markdown格式,让问题和回答一目了然
对于一个新功能模块,个人认为是非常适合用 TRAE 来完成绝大部分代码的,对于新增类的实现和完善,几乎都可以由 AI 来完成。最近的业务中正好就是这样一个例子:全新的功能,与现有业务代码的耦合度较低,实践下来大约有 90% 的代码可以用 AI 生成的,使用的 LLM 主要是:Gemini-2.5-Pro-Preview(0506)(最新版本中已经是 Gemini-2.5-Pro)和 DeepSeek-V3-0324。
下面列举一些实现过程中的典型 Case:
我一般习惯在技术方案中就把实现一个功能模块所需的类都设计好,然后再开始写代码,现在有了这份类图的指导,就可以使用 TRAE 快速地把关键类的基础框架搭建起来了。
如果方法和属性定义的比较清晰,可以直接将该类图截图给 Gemini 作为上下文,节省写提示词的时间,然后在对话时使用 @Builder 直接生成类的文件。每轮对话最好只处理一个文件,然后针对这个类的实现提具体的要求,例如:
import Foundation
classXXXStateManager {
static let sharedInstance = XXXStateManager()
private init() {}
private var _value: Float = 0.0
private let lock = NSLock()
var value: Float {
get {
lock.lock()
defer { lock.unlock() }
return _value
}
set {
lock.lock()
_value = newValue
lock.unlock()
}
}
func getXXXValue() -> Float {
return value
}
func getXXXState() {
}
func requestUpdateXXXValue() {
}
func syncXXXValue() {
}
func resetXXXToValue(_ newValue: Float) {
self.value = newValue
}
}
之后在实现每个类的过程中,就是不断地拆解问题,让 AI 去完成,一个问题解决后最好再开一个新的会话解决下一个问题,避免 LLM 对上下文理解的干扰,记得当新增代码量到一定程度时要查看是否有编译错误。
Case 2: 用 Figma 中的 UIKit 代码生成视图代码
以前这种画 UI 又枯燥又磨人的活,终于可以在几分钟内就搞定了~
我在这里的做法是,在 Figma 中选中图层,将对应的 UIKit 代码直接复制到 TRAE 中让 AI 去生成 Swift 代码。先实现视图本身的样式,再往里添加子视图元素,生成的代码基本不用做太大改动。
import UIKit
import SnapKit
classReadyCardView: UIView {
private let iconView = UIView()
private let titleLabel = UILabel()
private let subTitleLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
setupSubviews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
setupSubviews()
}
private func setupView() {
self.clipsToBounds = false
self.layer.cornerRadius = 16
let shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: 16)
layer.shadowPath = shadowPath.cgPath
layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.25).cgColor
layer.shadowOpacity = 1
layer.shadowRadius = 16
layer.shadowOffset = CGSize(width: 0, height: 8)
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [
UIColor(red: 0, green: 0, blue: 0, alpha: 1).cgColor,
UIColor(red: 0, green: 0, blue: 0, alpha: 1).cgColor,
UIColor(red: 0, green: 0.267, blue: 0.209, alpha: 1).cgColor
]
gradientLayer.locations = [0, 0.6, 1]
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
gradientLayer.frame = bounds
gradientLayer.cornerRadius = 16
layer.insertSublayer(gradientLayer, at: 0)
layer.borderWidth = 1.5
layer.borderColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0.15).cgColor
}
private func setupSubviews() {
addSubview(iconView)
iconView.snp.makeConstraints { make in
make.width.height.equalTo(120)
make.top.equalToSuperview().offset(64)
make.centerX.equalToSuperview()
}
addSubview(titleLabel)
titleLabel.textColor = UIColor(red: 1, green: 1, blue: 1, alpha: 1)
titleLabel.font = UIFont(name: "PingFangSC-Medium", size: 20)
titleLabel.textAlignment = .center
titleLabel.text = "已就绪"
titleLabel.snp.makeConstraints { make in
make.width.equalTo(210)
make.height.equalTo(28)
make.top.equalTo(iconView.snp.bottom).offset(8)
make.centerX.equalToSuperview()
}
addSubview(subTitleLabel)
subTitleLabel.textColor = UIColor(red: 0.604, green: 0.604, blue: 0.604, alpha: 1)
subTitleLabel.font = UIFont(name: "PingFangSC-Regular", size: 15)
subTitleLabel.text = ""
subTitleLabel.snp.makeConstraints { make in
make.width.equalTo(270)
make.height.equalTo(21)
make.top.equalTo(titleLabel.snp.bottom)
make.centerX.equalToSuperview()
}
}
override func layoutSubviews() {
super.layoutSubviews()
if let gradientLayer = layer.sublayers?.first as? CAGradientLayer {
gradientLayer.frame = bounds
}
}
}
这种方式简单直接,零成本直接上手使用。更进一步,我们也可以通过 Figma 官方提供的 MCP 工具来实现 UI 代码的生成,具体操作会在进阶篇中介绍。
Case 3: 生成读取 Settings 配置的代码
对于读取 Settings 配置的代码也是比较模板化的,如果手动去敲,在配置项很多的时候也要花费一些时间,现在描述清楚要求后,静待 TRAE 来生成代码就行了。下面左侧是生成的代码效果,几乎不需要改什么可以直接用。之后可以继续让 TRAE 去用这份 Configuration 替换掉写死在代码中的值。
import Foundation
import AWEAppSettings
classXXXConfiguration {
privatestatic let kXXXTextConfigRootKey = "XXX_text_config"
privatestatic let kXXXPlayDurationMinKey = "XXX_play_duration_min"
privatestatic let kXXXBannerShowDurationKey = "XXX_banner_show_duration"
privatestatic let kXXXClickTabRequestIntervalKey = "XXX_click_tab_request_interval"
privatestatic let kXXXClickMatchBellExpandBannerCountKey = "XXX_click_match_bell_expand_banner_count"
privatestatic let kTextConfigEntranceKey = "entrance"
privatestatic let kTextConfigCollectKey = "collect"
privatestatic let kTextConfigCollectTitleKey = "title"
privatestatic let kTextConfigCollectSubTitleKey = "sub_title"
privatestatic let kTextConfigFullKey = "full"
privatestatic let kTextConfigFullTitleKey = "title"
privatestatic let kTextConfigFullSubTitleKey = "sub_title"
privatestatic let kTextConfigFullBtnTextKey = "btn_text"
privatestatic let defaultTextConfigEntrance = ""
privatestatic let defaultTextConfigCollectTitle = ""
privatestatic let defaultTextConfigCollectSubTitle = ""
privatestatic let defaultTextConfigFullTitle = ""
privatestatic let defaultTextConfigFullSubTitle = ""
privatestatic let defaultTextConfigFullBtnText = ""
privatestatic let defaultCollectDictionary: [String: String] = [
kTextConfigCollectTitleKey: defaultTextConfigCollectTitle,
kTextConfigCollectSubTitleKey: defaultTextConfigCollectSubTitle
]
privatestatic let defaultFullDictionary: [String: String] = [
kTextConfigFullTitleKey: defaultTextConfigFullTitle,
kTextConfigFullSubTitleKey: defaultTextConfigFullSubTitle,
kTextConfigFullBtnTextKey: defaultTextConfigFullBtnText
]
static func playDurationMin() -> Int {
return AWEAppSettings.awemeSettings().integer(forKeyPath: kXXXPlayDurationMinKey, defaultValue: 3000)
}
static func bannerShowDuration() -> Int {
return AWEAppSettings.awemeSettings().integer(forKeyPath: kXXXBannerShowDurationKey, defaultValue: 10)
}
static func clickTabRequestInterval() -> Int {
return AWEAppSettings.awemeSettings().integer(forKeyPath: kXXXClickTabRequestIntervalKey, defaultValue: 30)
}
static func clickMatchBellExpandBannerCount() -> Int {
return AWEAppSettings.awemeSettings().integer(forKeyPath: kXXXClickMatchBellExpandBannerCountKey, defaultValue: 3)
}
privatestatic func getTextConfigStringValue(forKey subKey: String, defaultValue: String) -> String {
let keyPath = "\(kXXXTextConfigRootKey).\(subKey)"
return AWEAppSettings.awemeSettings().string(forKeyPath: keyPath, defaultValue: defaultValue) ?? defaultValue
}
privatestatic func getTextConfigDictionaryValue(forKey subKey: String, defaultValue: [String: Any]) -> [String: Any] {
let keyPath = "\(kXXXTextConfigRootKey).\(subKey)"
return AWEAppSettings.awemeSettings().dictionary(forKeyPath: keyPath, defaultValue: defaultValue) ?? defaultValue
}
static func textConfigEntrance() -> String {
return getTextConfigStringValue(forKey: kTextConfigEntranceKey, defaultValue: defaultTextConfigEntrance)
}
static func textConfigCollectDictionary() -> [String: String] {
let dict = getTextConfigDictionaryValue(forKey: kTextConfigCollectKey, defaultValue: defaultCollectDictionary)
return dict as? [String: String] ?? defaultCollectDictionary
}
static func textConfigCollectTitle() -> String {
let keyPath = "\(kTextConfigCollectKey).\(kTextConfigCollectTitleKey)"
return getTextConfigStringValue(forKey: keyPath, defaultValue: defaultTextConfigCollectTitle)
}
static func textConfigCollectSubTitle() -> String {
let keyPath = "\(kTextConfigCollectKey).\(kTextConfigCollectSubTitleKey)"
return getTextConfigStringValue(forKey: keyPath, defaultValue: defaultTextConfigCollectSubTitle)
}
static func textConfigFullDictionary() -> [String: String] {
let dict = getTextConfigDictionaryValue(forKey: kTextConfigFullKey, defaultValue: defaultFullDictionary)
return dict as? [String: String] ?? defaultFullDictionary
}
static func textConfigFullTitle() -> String {
let keyPath = "\(kTextConfigFullKey).\(kTextConfigFullTitleKey)"
return getTextConfigStringValue(forKey: keyPath, defaultValue: defaultTextConfigFullTitle)
}
static func textConfigFullSubTitle() -> String {
let keyPath = "\(kTextConfigFullKey).\(kTextConfigFullSubTitleKey)"
return getTextConfigStringValue(forKey: keyPath, defaultValue: defaultTextConfigFullSubTitle)
}
static func textConfigFullBtnText() -> String {
let keyPath = "\(kTextConfigFullKey).\(kTextConfigFullBtnTextKey)"
return getTextConfigStringValue(forKey: keyPath, defaultValue: defaultTextConfigFullBtnText)
}
}
在实现功能的过程中,可以借助 TRAE 随时对代码进行优化。比如这次开发中,异步读写 storage 在两处都有同样的处理,我让 AI 直接封装了一个AsyncStorageUtil类。再比如,我就是看某段代码写的很啰嗦,感觉可以优化,但是没有思路,也可以直接让 AI 给优化建议。
把大段的 OC 代码手动翻译成 Swift 还是会费一番功夫的,现在 AI 可以秒完成,在我们写 Swift 时一定要利用上。
Case 6: 使用 TRAE 的 cue 功能直接写代码
除了在对话框中与 AI 沟通来完成任务,很多时候我也会直接在 TRAE 中写代码的,按 Tab 键即可接受预测的内容,按 ESC 键取消当前的预测,体验下来接受的概率还是比较高的,这种方式也推荐大家去试用。
在开发中我们会遇到的一种情况是,去已有代码里 Copy 一段逻辑到自己的代码里,有时这段代码需要额外引入很多头文件,纯靠记忆精准敲出这些头文件是有一点难为我胖虎了,去原来的代码一点点找又比较费时,尤其是那个文件引入了一屏幕都放不下的头文件时。
此时就可以让 AI 先去干一番活啦,比如我要把下面的代码复制到一个新文件中去:
当我们遇到不能一眼看出某个方法在干什么的时候,可以让 LLM 详细解释这个方法,并生成流程图,比如下面这段代码,我们让 Gemini 来分析解释一下,并让它生成可以在 Mermaid 或 draw.io 中展示的流程图:
- (void)fetchRecommendCardOfFamiliarFeedWithSuccessCompletion:(void(^)(BOOL))completion {
if (![self enableInsertFamiliarFeedCard]) {
AWEBLOCK_INVOKE(completion,NO);
return;
}
dispatch_group_t group = dispatch_group_create();
BOOL fetchDataSuccess = NO;
if ([self.familiarFeedRecommendStrategy enableInsertFamiliarFeedObdCardAB] &&
self.familiarFeedObdDataSource.models.count == 0) {
fetchDataSuccess = YES;
dispatch_group_enter(group);
[self fetchFamiliarRecommendUserDataWithType:AWERecommendTypeFeedObdCard isAutoTrigger:NO completion:^{
dispatch_group_leave(group);
}];
}
if ([self.familiarFeedRecommendStrategy enableInsertFamiliarFeedMateCardAB] &&
self.familiarFeedMateDataSource.models.count == 0) {
fetchDataSuccess = YES;
dispatch_group_enter(group);
[self fetchFamiliarRecommendUserDataWithType:AWERecommendTypeFeedMateCard isAutoTrigger:NO completion:^{
dispatch_group_leave(group);
}];
}
if ([self.familiarFeedRecommendStrategy enableInsertFamiliarFeedListCardAB] &&
self.familiarFeedMultiMateDataSource.models.count == 0) {
fetchDataSuccess = YES;
dispatch_group_enter(group);
[self fetchFamiliarRecommendUserDataWithType:AWERecommendTypeFeedMateMultiCard isAutoTrigger:NO completion:^{
dispatch_group_leave(group);
}];
}
if (fetchDataSuccess) {
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
AWEBLOCK_INVOKE(completion, YES);
});
} else {
AWEBLOCK_INVOKE(completion, NO);
}
}
可以从 TRAE 的插件市场中安装一个 Mermaid Preview 插件,然后创建一个 mermaid.md,把代码贴过去就可以预览效果了,会根据当前的IDE给对应的配色。
在做技术调研时也经常会遇到要去了解一个完整功能模块实现过程,如果这个模块的代码比较内聚,并且都集中放在一个文件夹内,那就可以把整个文件夹丢给 AI 去分析,并让它生成流程图和时序图,但也要考虑到 LLM 一次可以接收的上下文长度。比如之前调研主框架的异形卡实现过程:
这样了解一番后,对这个模块是怎么工作的就有一个大致的了解,提升我们调研的效率。
在做代码重构时,也可以让 TRAE 来辅助完成,比如一些文件重名替换、抽离定义常量等等。前段时间把一个气泡视图重构成了可以给其他场景使用的通用组件,也是用 TRAE 完成了大部分工作,基本上自己想好了重构方案后,就是指挥 AI 来一步步干活了。
我这里直接新开一个 TRAE 窗口,只打开要修改的文件夹,然后就可以引用 #Workspace 了,也避免了其他代码上下文对 LLM 分析的干扰。
AI 修改后需要再审查核对一下,整体上比纯人工来写提效了很多,在我这次重构中至少节约 1 pd。
TRAE 已显著提升我的开发效率,从精准打开特定文件夹避免干扰,到助力新功能模块的高效搭建,它正重塑我们的开发模式。点击阅读原文,体验 TRAE 的智能编码,客户端开发比你想象的更简单。
下篇文章将深入挖掘更多实用技巧,为大家带来更丰富的开发灵感,敬请期待!