Log Entry

OC 项目中混编swift

如何在oc 中使用 swift 编写的类

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`

原因: 有以下几种情况:

  1. Swift 文件不在主 Target 里:Swift 文件的 Target Membership 未勾选主 Target。
    • 解决:在 Xcode 右侧 File Inspector 里确认 Target Membership 已勾选。
  2. Build Settings 没有设置 Swift 版本:
    • 解决:在 Build Settings 里搜索 Swift Language Version,设置为 Swift 5。
  3. 在测试 Target 的 OC 文件里 import:测试 Target 没有 凪-Swift.h 的搜索路径。
    • 解决:不要在测试 Target 的 OC 文件里 import,改用 Swift 测试文件加 @testable import 凪。

问题 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 类没有区别。