SwiftUI实战:构建现代化iOS应用界面的架构思考

轻舟开发记
2026-01-29 16:17
阅读 2089

去年双11那会儿,我还在上一家公司做产品经理,天天和开发团队扯皮“这个按钮能不能再大2px”、“用户路径是不是太深了”。结果今年年初,我直接转岗成了iOS开发——没错,就是那个自己画原型、自己写代码、自己测Bug的斜杠青年。
深夜写代码成了我的日常节奏,咖啡当水喝,键盘敲到冒烟。三年多下来,项目上线了、KPI完成了,但总觉得少了点什么。最近开始认真考虑换个环境,于是狠狠补了一波SwiftUI,想把之前产品视角下的“理想界面”真正用代码实现出来。


为什么是SwiftUI?

坦白说,刚接触SwiftUI时我是抗拒的。毕竟之前用UIKit写了两年,各种delegate、dataSource、AutoLayout已经刻进DNA了。但现实很骨感:Apple在WWDC上反复强调“声明式UI是未来”,App Store审核也越来越偏爱符合VisionOS设计语言的应用。再加上我们团队新来的设计师动不动就甩来一套Figma文件,里面全是动态渐变、交互动效、响应式布局——UIKit硬写?怕不是要肝到明年。

更重要的是,作为曾经的产品经理,我深知界面即体验。用户不会关心你用了什么框架,他们只在乎滑得顺不顺、加载快不快、崩不崩溃。而SwiftUI天然支持响应式、状态驱动、跨平台(至少Apple全家桶),这不正好契合“现代化”的定义吗?


架构设计:别让View变成一锅粥

很多团队一上来就狂写@State@Binding,结果三个月后代码变成意大利面条——View里塞网络请求、业务逻辑、数据处理,改个颜色都要翻半天。我吃过这亏,所以这次从第一天就定下规矩:

View只负责展示,逻辑交给ViewModel,数据交给Repository

听起来像老生常谈?但执行起来真没那么简单。举个例子,我们有个“资产看板”页面,要展示用户的数字资产余额——对,这里就涉及区块链数据。链上查询慢、接口不稳定、还要处理钱包地址绑定,如果全塞进View里,那画面我不敢看。

于是我们搞了个分层架构:

View (SwiftUI)
  ↓
ViewModel (ObservableObject)
  ↓
Repository (负责区块链API调用 + 本地缓存)
  ↓
Network Layer / Wallet SDK

关键在于,ViewModel不直接持有任何UI组件,也不依赖UIKit。它只暴露@Published属性供View监听,比如:

class AssetViewModel: ObservableObject {
    @Published var balance: String = "Loading..."
    @Published var isLoading = false
    
    private let repository: AssetRepository
    
    init(repository: AssetRepository) {
        self.repository = repository
    }
    
    func fetchBalance(for address: String) {
        isLoading = true
        Task {
            do {
                let balance = try await repository.getBalance(from: address)
                await MainActor.run {
                    self.balance = balance.formatted()
                    self.isLoading = false
                }
            } catch {
                // 统一错误处理,比如弹Toast
                print("Failed to fetch balance: $error)")
                await MainActor.run {
                    self.isLoading = false
                }
            }
        }
    }
}

这样,View只需要一行代码就能绑定状态:

struct AssetView: View {
    @StateObject private var viewModel = AssetViewModel(repository: .live)
    
    var body: some View {
        VStack {
            if viewModel.isLoading {
                ProgressView()
            } else {
                Text("余额: \(viewModel.balance)")
                    .font(.title2)
                    .fontWeight(.semibold)
            }
        }
        .onAppear {
            viewModel.fetchBalance(for: "0x...") // 实际从用户上下文获取
        }
    }
}

工具链:提升效率的隐形推手

说到工具,我必须吐槽一下:很多团队还在用手动截图+人工核对做UI验收,效率低到令人发指。自从我引入了以下组合拳,前端同学终于不用半夜被产品经理call起来改间距了:

工具 用途 效果
swiftformat + swiftlint 代码格式化 & 规范检查 PR不再因缩进吵架
SnapshotTesting UI快照测试 自动捕获视觉回归
Xcode Previews 实时预览 设计师直接看效果,不用等编译

特别是Xcode Previews,简直是跨部门协作神器。以前设计师提需求:“这个卡片阴影要更柔和一点”,我得改完打包发TestFlight,等他反馈再改。现在直接在Preview里调参数,实时看到效果,他坐我旁边都能点头说“就这个!”

struct AssetCard_Previews: PreviewProvider {
    static var previews: some View {
        AssetCard(balance: "5.23 ETH")
            .preferredColorScheme(.light)
        
        AssetCard(balance: "5.23 ETH")
            .preferredColorScheme(.dark)
    }
}

运营需求?别让代码变成打补丁现场

上周五晚上,运营同学突然找我:“老板说下周要上线一个活动Banner,能加个轮播图吗?最好还能埋点统计点击率。”
我差点一口老血喷出来——这都快下班了,还来需求?但转念一想,这不正是考验架构弹性的时候?

如果View里硬编码Banner逻辑,下次换活动又得改代码、走审核,黄花菜都凉了。所以我们搞了个可配置化运营组件

struct OperationalBannerView: View {
    let config: BannerConfig // 从远程配置中心拉取
    
    var body: some AssistantView {
        if config.isEnabled {
            AsyncImage(url: config.imageUrl) { image in
                image
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .onTapGesture {
                        trackEvent("banner_click", params: ["id": config.id])
                        openURL(config.targetURL)
                    }
            } placeholder: {
                Rectangle().foregroundColor(.gray.opacity(0.2))
            }
        }
    }
}

BannerConfig结构体由后端通过Firebase Remote Config或自建配置服务下发,包含图片URL、跳转链接、是否启用等字段。这样一来,运营改Banner再也不用求开发,真正实现“所见即所得”。


App Store审核那些坑

说到上架,Apple最近对区块链相关应用审核越来越严。我们第一次提交时,因为没在设置里提供“清除钱包缓存”选项,直接被拒。理由是:“App collects user data without clear deletion mechanism.”

后来学乖了,在SwiftUI里专门做了个隐私设置页面:

struct PrivacySettingsView: View {
    @AppStorage("walletAddress") private var walletAddress: String = ""
    
    var body: some View {
        Form {
            Section("账户安全") {
                Button("解绑钱包", role: .destructive) {
                    walletAddress = ""
                    // 同时清除Keychain中的私钥引用
                    WalletManager.shared.clearSession()
                }
            }
        }
        .navigationTitle("隐私与安全")
    }
}

另外,Apple明确要求不能有“挖矿”、“代币交易”等诱导性文案。我们原本有个按钮叫“立即挖矿”,改成“查看收益”才过审。这些细节,光靠技术不够,还得懂运营合规。


写在最后:从产品到开发的视角转换

回头看这段SwiftUI实战经历,最大的收获不是写了多少行代码,而是用工程思维重新理解用户体验。以前做产品时,总觉得“加个功能很简单”;现在写代码才知道,一个看似简单的交互动画,背后可能是性能、兼容性、可维护性的多重权衡。

如果你也在考虑转型,或者正在用SwiftUI重构老项目,我的建议是:
别只盯着语法糖,先想清楚架构。 声明式UI的优势在于“描述是什么”,而不是“怎么做”。把状态管理、数据流、副作用隔离清楚,后期迭代才能游刃有余。

至于跳槽?我已经投了几家Web3方向的公司,他们正好需要既懂产品又会写SwiftUI的人——毕竟,能把区块链数据优雅地展示在iOS界面上,还不被App Store拒掉,这种人不多 😏

深夜码字完毕,咖啡见底,明天继续肝。希望这篇带点人味儿的分享,能帮你在现代化iOS开发路上少踩几个坑。

评论 0

最热最新
暂无评论
轻舟开发记Lv.1
0
影响力
0
文章
0
粉丝