You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Swift #Observable宏结合Objective-C协议实现双向属性更新?

问题描述

我正在SwiftUI中使用Objective-C编写的模型,目标是既能通过Swift(比如TextField)修改Car实例属性,也能通过Objective-C代码(比如调用car.changeColor())修改属性后同步更新SwiftUI界面。目前Swift端修改能正常刷新界面,但调用OC的changeColor()方法时界面无更新。我采用面向协议的设计,需要基于协议实现,想请教如何用WWDC23推出的@Observable宏实现预期效果。

Objective-C模型代码

@protocol CarProtocol <NSObject>
@property (strong, readwrite) NSString * color;
- (void)changeColor;
@end

@protocol PersonProtocol <NSObject>
@property (strong, readwrite) NSString * name;
- (void)changeName;
@end

@interface Car : NSObject <CarProtocol>

@property (strong, readwrite) NSString * color;

- (instancetype)initWithColor:(NSString *)color;
- (void)changeColor;

@end


@implementation Car

- (instancetype)init {
    if ((self = [super init])) {
        self.color = @"";
    }
    return self;
}


- (instancetype)initWithColor:(NSString *)color {
    if ((self = [super init])) {
        self.color = color;
    }
    return self;
}

- (void)changeColor {
    NSArray *colors = @[@"Red", @"Blue", @"Green", @"Yellow", @"Black", @"White"];
    NSString *newColor = self.color;
    
    while (self.color == nil || [newColor isEqualToString:self.color]) {
        newColor = colors[arc4random_uniform((uint32_t)colors.count)];
    }
    
    self.color = newColor;
    NSLog(@"The car color has been changed to %@", self.color);
}

@end

我尝试的Swift代码(存在界面不更新问题)

import SwiftUI
import Combine

extension Car: ObservableObject {
    public var objectWillChange: AnyPublisher<Void, Never> {
        publisher(for: \.color, options: .prior)
            .map { _ in }
            .eraseToAnyPublisher()
    }
    
    public static func randomCar() -> Car {
        let colors = ["Red", "Blue", "Green", "Yellow", "Purple", "Orange"]
        return Car(color: colors.randomElement() ?? "Red")
    }
}


@Observable
class ObservableCar<CarT>
    where CarT: CarProtocol {
    public var car: CarT
    
    public init(_ car: CarT) {
        self.car = car
    }
}

struct SwiftObservableView<CarT>: View
    where CarT: CarProtocol & ObservableObject {
    @State var car: ObservableCar<CarT>
    
    public init(car: CarT) {
        self.car = ObservableCar(car)
    }
    
    var body: some View {
        NavigationStack {
            Form {
                Section(header: Text("Modification Form")) {
                    VStack {
                        TextField("Color", text: $car.car.color)
                    }
                }
                Section(header: Text("Results")) {
                    VStack {
                        HStack() {
                            Text("Color")
                            Spacer()
                            Text(car.car.color)
                        }
                    }
                }
                Section(header: Text("Actions")) {
                    Button("Random Car") {
                        self.car = ObservableCar<CarT>(Car.randomCar() as! CarT)
                    }
                    Button("Assign from Objective-C") {
                        self.car.car.changeColor()
                    }
                }
            }
            .navigationTitle("Car Data (Swift)")
        }
    }
}

#Preview {
    SwiftObservableView(car: Car(color: "Green"))
}

附:我用Combine实现的非协议方案(仅作参考)

import SwiftUI
import Combine

extension PersonCombine: ObservableObject {
    public var objectWillChange: AnyPublisher<Void, Never> {
        publisher(for: \.name, options: .prior)
            .map { _ in }
            .eraseToAnyPublisher()
    }
}

extension CarCombine: ObservableObject {
    public var objectWillChange: AnyPublisher<Void, Never> {
        Publishers.Merge3(
              owner.objectWillChange,
              publisher(for: \.owner, options: .prior).map { _ in },
              publisher(for: \.color, options: .prior).map { _ in }
          )
          .eraseToAnyPublisher()
    }
    
    public static func randomCar() -> CarCombine {
        let colors = ["Red", "Blue", "Green", "Yellow", "Purple", "Orange"]
        return CarCombine(color: colors.randomElement() ?? "Red")
    }
}

struct CombineView: View {
    @StateObject var car: CarCombine
    
    var body: some View {
        NavigationStack {
            Form {
                Section(header: Text("Modification Form")) {
                    VStack {
                        TextField("Color", text: $car.color)
                    }
                    VStack {
                        TextField("Owner Name", text: $car.owner.name)
                    }
                }
                Section(header: Text("Results")) {
                    VStack {
                        HStack {
                            Text("Color")
                            Spacer()
                            Text(car.color)
                        }
                    }
                    VStack {
                        HStack {
                            Text("Owner")
                            Spacer()
                            Text(car.owner.name)
                        }
                    }
                }
                Section(header: Text("Actions")) {
                    Button("Random Car") {
                        let randomCar = CarCombine.randomCar()
                        car.color = randomCar.color
                    }
                    Button("Assign Car.Color from Objective-C") {
                        car.changeColor()
                    }
                    Button("Assign Person.Name from Objective-C") {
                        car.owner.changeName()
                    }
                }
                Section(header: Text("Owner Details")) {
                    NavigationLink(destination: PersonDetailCombineView(person: car.owner)) {
                        HStack {
                            Text("Owner")
                            Spacer()
                            Text(car.owner.name)
                        }
                    }
                }
            }
            .navigationTitle("Car Data (Combine)")
        }
    }
}

#Preview {
    CombineView(car: CarCombine())
}

解决方案

核心问题在于:OC对象本身不支持@Observable宏的自动观察,且你之前的ObservableCar包装类无法感知OC内部修改属性的动作。需要通过KVO监听OC属性变化,再结合@Observable的手动通知来触发界面更新。

步骤1:修改协议,确保OC属性支持KVO

OC默认通过@property合成的属性自带KVO支持,因此原CarProtocol无需额外修改,直接使用即可。

步骤2:使用@Observable包装OC模型,添加KVO监听

替换原来的ObservableCar和Car的ObservableObject扩展,用@Observable类包装OC实例,并监听属性变化:

import SwiftUI

@Observable
class ObservableCar<CarT: CarProtocol> {
    var car: CarT
    private var observation: NSKeyValueObservation?
    
    init(_ car: CarT) {
        self.car = car
        // 监听color属性变化,触发Observable的界面更新通知
        observation = car.observe(\.color, options: [.new]) { [weak self] _, _ in
            self?.objectWillChange.send()
        }
    }
    
    // 封装OC的changeColor方法,统一调用入口
    func changeColor() {
        car.changeColor()
    }
    
    // 保持随机生成Car的静态方法
    static func randomCar() -> ObservableCar<Car> {
        let colors = ["Red", "Blue", "Green", "Yellow", "Purple", "Orange"]
        return ObservableCar(Car(color: colors.randomElement() ?? "Red"))
    }
}

步骤3:修改SwiftUI视图,直接使用包装类

简化视图代码,不再依赖OC类实现ObservableObject,而是直接使用ObservableCar:

struct SwiftObservableView<CarT: CarProtocol>: View {
    @State var car: ObservableCar<CarT>
    
    init(car: CarT) {
        self.car = ObservableCar(car)
    }
    
    var body: some View {
        NavigationStack {
            Form {
                Section(header: Text("Modification Form")) {
                    TextField("Color", text: $car.car.color)
                }
                Section(header: Text("Results")) {
                    HStack {
                        Text("Color")
                        Spacer()
                        Text(car.car.color)
                    }
                }
                Section(header: Text("Actions")) {
                    Button("Random Car") {
                        if let randomCar = ObservableCar.randomCar() as? ObservableCar<CarT> {
                            self.car = randomCar
                        }
                    }
                    Button("Assign from Objective-C") {
                        car.changeColor()
                    }
                }
            }
            .navigationTitle("Car Data (Swift)")
        }
    }
}

#Preview {
    SwiftObservableView(car: Car(color: "Green"))
}

关键说明

  1. KVO监听:通过NSKeyValueObservation监听OC对象的color属性变化,当OC内部调用changeColor()修改color时,会触发KVO回调,此时调用objectWillChange.send()通知@Observable更新界面。
  2. 包装类封装:将OC的方法封装到ObservableCar中,统一入口,确保所有修改动作都能被监听。
  3. 面向协议兼容:包装类的泛型约束依然基于CarProtocol,符合你的面向协议设计要求。

扩展:处理多属性或嵌套对象

如果你的OC模型有多个属性或者嵌套的PersonProtocol对象,可以扩展KVO监听:

@Observable
class ObservablePerson<PersonT: PersonProtocol> {
    var person: PersonT
    private var observation: NSKeyValueObservation?
    
    init(_ person: PersonT) {
        self.person = person
        observation = person.observe(\.name, options: [.new]) { [weak self] _, _ in
            self?.objectWillChange.send()
        }
    }
    
    func changeName() {
        person.changeName()
    }
}

// 嵌套使用示例
@Observable
class ObservableCarWithOwner<CarT: CarProtocol, PersonT: PersonProtocol> {
    var car: CarT
    var owner: ObservablePerson<PersonT>
    private var carObservation: NSKeyValueObservation?
    
    init(car: CarT, owner: PersonT) {
        self.car = car
        self.owner = ObservablePerson(owner)
        carObservation = car.observe(\.color, options: [.new]) { [weak self] _, _ in
            self?.objectWillChange.send()
        }
    }
}

内容的提问来源于stack exchange,提问作者OzB

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 22:30:55