Xcode14下低部署目标高版本类型存储属性适配方案
在Xcode 14及更早版本(对应Swift 5.7及更低版本)中,开发者可通过lazy var创建数据类型在当前部署目标下不可用的存储属性。例如RelativeDateTimeFormatter是iOS 13新增API,若要在最低支持iOS 12的应用中使用该类,此前可编写如下代码:
@available(iOS 13.0, *) private lazy var relativeDateTimeFormatter: RelativeDateTimeFormatter = { let formatter = RelativeDateTimeFormatter() formatter.dateTimeStyle = .numeric formatter.unitsStyle = .short formatter.formattingContext = .standalone return formatter }()
调用该属性时可通过if #available做版本分支判断,在iOS 13以下系统中使用其他日期格式化器。由于日期格式化器的创建开销较高,必须保证仅初始化一次并复用(例如列表Cell数据填充场景),此前的存储属性写法可很好满足该需求——但该写法在新版Xcode中会触发如下编译错误:
Stored properties cannot be marked potentially unavailable with ‘@available’
Xcode 14发布说明中明确指出:
Swift中的存储属性不能持有运行时可能不可用的类型信息。但在Swift 5.7之前,当存储属性带有lazy修饰符或关联属性包装器时,编译器会错误接受存储属性上的
@available标记,该问题可能导致应用在旧版操作系统上运行崩溃。当前Swift编译器已统一拒绝所有存储属性上的@available标记。
同类场景非常常见:比如为应用内的视图控制器添加Live Text(实况文本)支持,需要创建ImageAnalyzer实例并将其作为视图控制器的属性持有。但该API是iOS 16新增能力,如下旧写法已无法通过编译:
@available(iOS 16.0, *) private(set) lazy var analyzer = ImageAnalyzer()
以下方案均经过生产环境验证,可保证实例仅初始化一次,同时兼容低版本系统,不会触发编译错误或运行时崩溃。
方案1:手动实现可选存储的懒加载
这是最通用、零依赖的方案,核心逻辑是将实际存储用可选类型(或Any类型)声明为不带版本标记的存储属性,再通过带版本标记的计算属性控制初始化和访问逻辑。
针对RelativeDateTimeFormatter的实现代码:
// 实际存储,不添加@available标记 private var _relativeDateTimeFormatter: RelativeDateTimeFormatter? // 对外访问的计算属性,添加版本标记 @available(iOS 13.0, *) private var relativeDateTimeFormatter: RelativeDateTimeFormatter { if let existing = _relativeDateTimeFormatter { return existing } let formatter = RelativeDateTimeFormatter() formatter.dateTimeStyle = .numeric formatter.unitsStyle = .short formatter.formattingContext = .standalone _relativeDateTimeFormatter = formatter return formatter }
如果目标API的类型无法直接声明为可选存储(比如类型本身仅在高版本存在,低版本SDK中无该类型定义),可以用Any?作为底层存储类型,例如ImageAnalyzer场景的实现:
private var _analyzer: Any? @available(iOS 16.0, *) private(set) var analyzer: ImageAnalyzer { get { if let existing = _analyzer as? ImageAnalyzer { return existing } let instance = ImageAnalyzer() _analyzer = instance return instance } }
- 优势:逻辑完全可控,不需要额外封装,兼容所有Swift版本
- 注意:所有业务代码不要直接访问带下划线前缀的底层存储属性,必须通过带版本标记的计算属性访问,且访问前必须通过
if #available做版本校验。
方案2:静态全局存储
如果高版本API的实例需要在多处复用,可以将其定义在枚举/结构体的静态属性中:
enum SharedFormatter { @available(iOS 13.0, *) static let relativeDateTimeFormatter: RelativeDateTimeFormatter = { let formatter = RelativeDateTimeFormatter() formatter.dateTimeStyle = .numeric formatter.unitsStyle = .short formatter.formattingContext = .standalone return formatter }() }
Swift的静态let属性默认是懒加载的,且全局只会初始化一次。只要访问该属性前做了版本判断,低版本系统就不会触发该类型的加载,不会出现运行时崩溃。
- 优势:写法最简洁,天然保证单例,适合全局复用的工具类实例
- 注意:不要在没有版本校验的上下文里访问该静态属性,否则会在低版本系统启动时触发类型加载导致崩溃。
方案3:通用属性包装器封装
如果项目中这类需求较多,可以封装一个通用的属性包装器,把单次初始化的逻辑统一收口,减少重复代码:
@propertyWrapper class AvailableLazy<T> { private var storage: T? private let build: () -> T init(wrappedValue build: @autoclosure @escaping () -> T) { self.build = build } var wrappedValue: T { if let instance = storage { return instance } let newInstance = build() storage = newInstance return newInstance } }
使用时和原来的lazy写法几乎一致,只需要注意@available标记要加在对外暴露的属性上,不要加在底层存储逻辑上:
@available(iOS 16.0, *) @AvailableLazy private(set) var analyzer = ImageAnalyzer()
- 优势:调用侧写法和旧的lazy写法几乎无差异,复用性高
- 注意:属性包装器本身不要添加任何版本限制,保证底层存储逻辑在所有系统版本都能正常运行。
内容的提问来源于stack exchange,提问作者Jordan H

