OC 项目中混编swift
本文介绍如何在 Objective-C 项目里引入 Swift 类,以及这个过程中会踩到的坑。
为什么需要 OC 调用 Swift
在项目中,需要一个第三方库
有些库只有 Swift 版本,没有 OC 版
项目中选了 Down 做 Markdown 渲染,Down 是纯 Swift 库,必须从 Swift 中 import。因此需要用 Swift 写一个中间层,再让 OC 调用。
核心机制:自动生成的桥接头文件
Xcode 编译时,会自动把 Swift 文件里所有标注了 @objc 的类、方法、属性,生成一份 OC 头文件:
[你的 Target 名]-Swift.h比如 target 叫 凪,生成的文件就是 凪-Swift.h。
这个文件不在磁盘上,是 Xcode 在编译期动态生成放在 DerivedData 里的。OC 文件 #import 它即可调用里面的所有 Swift 类。
Swift 文件
└── @objc public class Foo: NSObject
└── Xcode 编译时自动生成 凪-Swift.h
└── OC 文件 #import "凪-Swift.h"
└── [Foo renderXxx] 正常调用关键限制: Swift 类必须继承自 NSObject,且加上 @objc 标注,才会出现在生成的头文件里。
完整配置步骤
步骤 1:确认项目有 Swift 文件(或新建一个)
直接在 Xcode 里 File → New → File → Swift File,Xcode 会弹出提示:
"Would you like to configure an Objective-C bridging header?"
选 Create Bridging Header,Xcode 自动创建 [Target]-Bridging-Header.h。
注意: 这里的 Bridging Header 是 OC → Swift 方向(让 Swift 调用 OC)。
OC 调用 Swift 方向不需要 Bridging Header,靠的是自动生成的[Target]-Swift.h。
步骤 2:确认 Build Settings 里有桥接头文件路径
在主 Target 的 Build Settings 中搜索 Bridging,确认 Objective-C Bridging Header 的值指向你的桥接头文件:
凪/凪-Bridging-Header.h同时确认 Swift Language Version 已设置(比如 5.0)。
步骤 3:写 Swift 中间层,标注 @objc
import Foundation
import SomeSwiftLibrary // 只有 Swift 能 import 这里
@objc public class MySwiftWrapper: NSObject {
@objc public static func doSomething(_ input: String) -> String {
// 调用 Swift 库的逻辑
return SomeSwiftLibrary.process(input)
}
}必须满足:
| 要求 | 原因 |
|---|---|
继承 NSObject |
OC 的对象模型基于 NSObject,不继承则无法桥接 |
加 @objc |
告诉编译器把这个类/方法写进生成的头文件 |
加 public |
头文件是跨模块的,private/internal 不会出现在里面 |
步骤 4:在 OC 文件中 import 并调用
#import "凪-Swift.h"// import 自动生成的头文件
NSString *result = [MySwiftWrapper doSomething:@"input"];实战演练:XCAIChatMarkdownRenderer
背景
Down 是 Swift 库,封装了 libcmark(CommonMark 官方 C 实现),只能从 Swift 中 import Down。项目主体是 OC,需要在 OC 的 AI Cell 里调用 Markdown 渲染。
Swift 中间层
文件:XCAIChatMarkdownRenderer.swift
import Foundation
import Down
// Markdown 渲染器,对外暴露纯 OC 接口。
// 内部采用段落级缓存,以段落内容 hash 为 key,避免流式场景下重复渲染。
@objc public class XCAIChatMarkdownRenderer: NSObject {
private static var paragraphCache: [Int: NSAttributedString] = [:]
// 将 Markdown 字符串渲染为 NSAttributedString。
@objc public static func renderMarkdown(_ markdown: String) -> NSAttributedString {
guard !markdown.isEmpty else { return NSAttributedString() }
let paragraphs = markdown.components(separatedBy: "\n\n")
let result = NSMutableAttributedString()
for (index, paragraph) in paragraphs.enumerated() {
let trimmed = paragraph.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { continue }
result.append(cachedRender(trimmed))
if index < paragraphs.count - 1 {
result.append(NSAttributedString(string: "\n"))
}
}
return result
}
private static func cachedRender(_ paragraph: String) -> NSAttributedString {
let key = paragraph.hashValue
if let cached = paragraphCache[key] { return cached }
let rendered = renderParagraph(paragraph)
paragraphCache[key] = rendered
return rendered
}
private static func renderParagraph(_ text: String) -> NSAttributedString {
do {
return try Down(markdownString: text).toAttributedString()
} catch {
return NSAttributedString(string: text)
}
}
}OC 调用方
#import "凪-Swift.h"
// AI Cell 渲染消息时调用
NSAttributedString *rendered = [XCAIChatMarkdownRenderer renderMarkdown:message.content];
self.textView.attributedText = rendered;对 OC 来说完全透明,和调普通 OC 类没有区别。
设计决策:段落级缓存
这里稍微提一下在流式输出场景下,md语法渲染的机制问题
流式输出场景下,AI 每次新增几个字就触发一次渲染。如果每次都全量渲染整段文本,性能浪费严重
按 \n\n 切分段落,已完成的段落缓存结果,只渲染新增内容:
消息内容:
"这是第一段\n\n这是第二段(正在输出中)"
第一段 → hash 命中缓存,直接取结果 ← 零开销
第二段 → 未完成,重新渲染 ← 只渲染这一段常见问题与解决方案
问题 1:****`'凪-Swift.h' file not found`
原因: 有以下几种情况:
- Swift 文件不在主 Target 里:Swift 文件的 Target Membership 未勾选主 Target。
- 解决:在 Xcode 右侧 File Inspector 里确认 Target Membership 已勾选。
- Build Settings 没有设置 Swift 版本:
- 解决:在 Build Settings 里搜索
Swift Language Version,设置为Swift 5。
- 解决:在 Build Settings 里搜索
- 在测试 Target 的 OC 文件里 import:测试 Target 没有
凪-Swift.h的搜索路径。- 解决:不要在测试 Target 的 OC 文件里 import,改用 Swift 测试文件加
@testable import 凪。
- 解决:不要在测试 Target 的 OC 文件里 import,改用 Swift 测试文件加
问题 2:****`error: Unable to find module dependency: 'libcmark'`
原因: Xcode 16 引入了显式模块构建(Explicit Module Build)。某些 CocoaPods 引入的 C 模块(如 Down 内部的 libcmark)在显式模块模式下无法被解析。
解决: 在 Build Settings 里关闭显式模块构建:
SWIFT_ENABLE_EXPLICIT_MODULES = NO如果用 CocoaPods,需要在 Podfile 的 post_install 里持久化这个设置(每次 pod install 都会重置 Build Settings):
post_install do |installer|
main_project = Xcodeproj::Project.open('凪.xcodeproj')
['凪', '凪Tests'].each do |target_name|
t = main_project.targets.find { |x| x.name == target_name }
next unless t
t.build_configurations.each do |config|
config.build_settings['SWIFT_ENABLE_EXPLICIT_MODULES'] = 'NO'
end
end
main_project.save
end问题 3:****`ld: framework 'Pods__' not found`
原因: Target 名包含非 ASCII 字符(如「凪」),CocoaPods 生成的 aggregate framework 产品名被截断为 Pods__,但该 framework 实际上不存在。
解决: 在 post_install 里手动清理这个无效引用:
post_install do |installer|
require 'xcodeproj'
main_project = Xcodeproj::Project.open('凪.xcodeproj')
# 清理 Frameworks Build Phase 里的无效引用
main_project.targets.each do |target|
phase = target.frameworks_build_phase
next unless phase
phase.files
.select { |f| f.file_ref&.path.to_s.start_with?('Pods__') }
.each { |f| phase.remove_build_file(f) }
end
# 从整个对象树中清理无效 PBXFileReference
main_project.objects
.select { |o|
o.is_a?(Xcodeproj::Project::Object::PBXFileReference) &&
o.path.to_s.start_with?('Pods__')
}
.each(&:remove_from_project)
main_project.save
end问题 4:****`SWIFT_VERSION '' is unsupported`
原因: 主 Target 有 Swift 文件后,测试 Target 也需要声明 SWIFT_VERSION,即使测试文件是 OC。
解决: 在 post_install 里同时为主 Target 和测试 Target 设置:
['凪', '凪Tests'].each do |target_name|
# 两个 target 都要设置
config.build_settings['SWIFT_VERSION'] = '5.0'
end问题 5:Swift 类在 OC 头文件里看不到
原因: 忘记加 @objc 或 public,或者没有继承 NSObject
// 错误:OC 看不到这个类
class MyClass {
func doSomething() { }
}
// 正确:OC 可以调用
@objc public class MyClass: NSObject {
@objc public func doSomething() { }
}注意: Swift 枚举要暴露给 OC,必须是整数类型!!!!
// 正确
@objc public enum MyState: Int {
case idle = 0
case active = 1
}
// 错误:OC 不支持非整数枚举
@objc public enum MyState: String { ... }注意事项
1. CocoaPods 下的 Build Settings 会被重置
每次运行 pod install,CocoaPods 会覆盖部分 Build Settings。需要通过 post_install 钩子在每次 install 后重新写入配置,而不是直接在 Xcode GUI 里改。
2.** **`@objc`** **的传递性
父类加了 @objc,子类不会自动继承。每个需要暴露的方法/属性都要单独标注,或者用 @objcMembers 一次性标注整个类:
// 一次性把所有成员都暴露给 OC
@objc @objcMembers public class MyClass: NSObject {
public var name: String = "" // 不用单独加 @objc
public func doWork() { } // 不用单独加 @objc
}3. Swift 泛型无法暴露给 OC
// 这个无法在 OC 中使用
@objc public class Container<T>: NSObject { }
// 解决:用具体类型替代泛型
@objc public class StringContainer: NSObject { }4. 循环 import 问题
凪-Swift.h 不能被 Bridging Header 引用,会导致循环依赖:
// 凪-Bridging-Header.h
#import "凪-Swift.h" // 错误!会导致编译错误Bridging Header 只放 OC 头文件(供 Swift 调用的 OC 类),Swift 头文件只在 OC 实现文件里 #import
5. 文件必须加入正确的 Target
Swift 文件的 Target Membership 必须包含主 Target(如「凪」),否则不会被编译进去,生成的 凪-Swift.h 里也不会有对应的类。
总结
| 步骤 | 操作 |
|---|---|
| 1 | 创建 Bridging Header(Xcode 首次添加 Swift 文件时自动提示) |
| 2 | 在 Build Settings 里设置 Swift Language Version |
| 3 | Swift 类继承 NSObject,标注 @objc public |
| 4 | OC 文件 #import "凪-Swift.h" 直接调用 |
| 5 | 用 CocoaPods 时,把关键 Build Settings 写入 post_install 持久化 |
整个机制的核心是 Xcode 自动生成的 [Target]-Swift.h,只要 Swift 侧标注正确,OC 侧调用和普通 OC 类没有区别。