很抱歉出现了一些模糊的标题,但我不确定更合适的标题是什么。
让我先解释一下我的设置和我想要达到的目标。
我定义了一个名为DeviceInterface的接口。我有两个符合该接口的对象:一个名为MockedDevice的模拟对象和名为DeviceImplementation的实际实现对象。
其计划是使用MockedDevice进行SwiftUI预览,并在模拟器中运行应用程序(当某些设备操作/值不可用)和DeviceImplementation用于设备执行。
问题出现在DeviceApp中,在这里,我用一个符合DeviceInterface的对象实例化主应用程序视图。我定义了一个DeviceInterface类型的泛型属性,我试图根据代码是在模拟器上执行还是在设备上执行来设置该属性。
当我试图将该属性传递给应用程序的主视图(使用符合接口ContentView的泛型类型初始化的ContentView)时,我会得到以下错误:
Value of protocol type 'DeviceInterface' cannot conform to 'DeviceInterface'; only struct/enum/class types can conform to protocols
将属性直接初始化为let device = DeviceImplementation(device: UIDevice.current)或let device = MockedDevice(device: UIDevice.current) (省略类型),然后传递该值,效果非常好,因此,我的问题似乎在于属性的类型定义。
我知道我可以稍微重新排列代码,并使用上面的工作实例化方法将ContentView实例化到#if TARGET_IPHONE_SIMULATOR中,其中省略了类型定义,但我想了解我做错了什么,以及如何使下面的代码工作。
请参阅下面的示例,以演示我正在尝试实现的目标。请记住,这是我要解决的问题的一个快速而简单的演示。
// MARK: - Interfaces
protocol DeviceInterface {
    var name: String { get }
}
protocol ObservableDevice: DeviceInterface, ObservableObject {}
// MARK: - Implementations
class MockedDevice: ObservableDevice {
    @Published
    var name: String  = ""
    
    init(name: String) {
        self.name = name
    }
}
class DeviceImplementation: ObservableDevice {
    @Published
    private(set) var name: String  = ""
    
    let device: UIDevice
    
    init(device: UIDevice) {
        self.device = device
        name = device.name
    }
}
// MARK: - App
@main
struct DeviceApp: App {
    var body: some Scene {
        WindowGroup {
            let device: ObservableDevice = {
                #if TARGET_IPHONE_SIMULATOR
                return MockedDevice(name: "Mocked device")
                #else
                return DeviceImplementation(device: UIDevice.current)
                #endif
            }()
            
            ContentView(device: device) // Generates error: Value of protocol type 'DeviceInterface' cannot conform to 'DeviceInterface'; only struct/enum/class types can conform to protocols
        }
    }
}
// MARK: - Main view
struct ContentView<T>: View where T: ObservableDevice {
    
    @ObservedObject
    private(set) var device: T
    
    var body: some View {
        Text("Hello World")
    }
    
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        let device = MockedDevice(name: "Mocked device")
        ContentView(device: device)
    }
}发布于 2021-03-15 19:04:02
首先,我如何真正做到这一点:子类化。您已经有了一个类,而“抽象”版本正是您的“模拟”版本。所以我会继续做一个子类:
// "Abstract" version (just set the values and they never change)
class Device: ObservableObject {
    @Published
    fileprivate(set) var name: String
    @Published
    fileprivate(set) var batteryLevel: Float
    init(name: String = "", batteryLevel: Float = -1) {
        self.name = name
        self.batteryLevel = batteryLevel
    }
}
// Self-updating version based on UIKit. Similar version could be made for WatchKit
class UIKitDevice: Device {
    private var token: NSObjectProtocol?
    init(device: UIDevice) {
        super.init(name: device.name, batteryLevel: device.batteryLevel)
        device.isBatteryMonitoringEnabled = true
        token = NotificationCenter.default.addObserver(forName: UIDevice.batteryLevelDidChangeNotification, 
                                                       object: device,
                                                       queue: nil) { [weak self] _ in
            self?.batteryLevel = device.batteryLevel
        }
    }
    deinit {
        NotificationCenter.default.removeObserver(token!)
    }
}那么device的定义是:
        let device: Device = {
            #if TARGET_IPHONE_SIMULATOR
            return Device(name: "Mocked device")
            #else
            return UIKitDevice(device: .current)
            #endif
        }()很简单。我喜欢它。
但我不是斯威夫特的超级粉丝。在这种情况下,它工作良好,但一般来说,继承不是一件伟大的事情,海事组织。那么,你将如何通过组合而不是继承来做到这一点呢?
首先,抽象一个可以更新电池信息的东西:
import Combine // For Cancellable
protocol BatteryUpdater {
    func addBatteryUpdater(update: @escaping (Float) -> Void) -> Cancellable
}并接受BatteryUpdater到设备(标记final只是为了证明我能做到;我不主张在所有地方洒final ):
final class Device: ObservableObject {
    @Published
    private(set) var name: String
    @Published
    private(set) var batteryLevel: Float
    private var batteryObserver: Cancellable?
    init(name: String = "", batteryLevel: Float = -1, batteryUpdater: BatteryUpdater? = nil) {
        self.name = name
        self.batteryLevel = batteryLevel
        batteryObserver = batteryUpdater?.addBatteryUpdater(update: { [weak self] level in
            self?.batteryLevel = level
        })
    }
    deinit {
        batteryObserver?.cancel()
    }
}因此,现在设备只是正常保存数据,但它可以要求它的电池水平被其他东西更新。它可以取消这个请求。我也可以在这里使用KeyPaths,或者其他更高级的组合工具,但这说明了这个想法。把改变的东西去掉,那就是“电池的价值是如何改变的”。不要抽象出没有改变的东西,那就是“我有一个电池级别,当它改变时通知观察者。”
有了这一点,请看追溯一致性的力量。UIDevice可以是一个BatteryUpdater:
extension UIDevice: BatteryUpdater {
    func addBatteryUpdater(update: @escaping (Float) -> Void) -> Cancellable {
        let nc = NotificationCenter.default
        let token = nc.addObserver(forName: UIDevice.batteryLevelDidChangeNotification,
                                   object: self, queue: nil) { _ in
            // This retains self as long as the observer exists. That's intentional
            update(self.batteryLevel)
        }
        return AnyCancellable {
            nc.removeObserver(token)
        }
    }
}使用方便的初始化程序可以轻松地从UIDevice创建:
extension Device {
    convenience init(device: UIDevice) {
        self.init(name: device.name, batteryLevel: device.batteryLevel, batteryUpdater: device)
    }
}现在,创建该设备如下所示:
        let device: Device = {
            #if TARGET_IPHONE_SIMULATOR
            return Device(name: "Mocked device")
            #else
            return Device(device: .current)
            #endif
        }()只是一直都是个装置。别嘲笑我。没有协议存在。没有仿制药。
如果有更多的东西需要更新,而不仅仅是BatteryLevel呢?不断地传递越来越多的闭包可能会变得很烦人。因此,您可以通过传递整个设备将BatteryUpdater转换为一个完整的DeviceUpdater:
protocol DeviceUpdater {
    func addUpdater(for device: Device) -> Cancellable
}设备基本上是一样的,只是添加proximityState来更新其他内容:
final class Device: ObservableObject {
    @Published
    private(set) var name: String
    @Published
    fileprivate(set) var batteryLevel: Float
    @Published
    fileprivate(set) var proximityState: Bool
    private var updateObserver: Cancellable?
    init(name: String = "", batteryLevel: Float = -1, proximityState: Bool = false,
         updater: DeviceUpdater? = nil) {
        self.name = name
        self.batteryLevel = batteryLevel
        self.proximityState = proximityState
        updateObserver = updater?.addUpdater(for: self)
    }
    deinit {
        updateObserver?.cancel()
    }
}UIDevice遵循同样的方式,只是通过直接更新device来实现“内到外”的方式。
extension UIDevice: DeviceUpdater {
    func addUpdater(for device: Device) -> Cancellable {
        let nc = NotificationCenter.default
        let battery = nc.addObserver(forName: UIDevice.batteryLevelDidChangeNotification,
                                   object: self, queue: nil) { [weak device] _ in
            device?.batteryLevel = self.batteryLevel
        }
        let prox = nc.addObserver(forName: UIDevice.proximityStateDidChangeNotification,
                                   object: self, queue: nil) { [weak device] _ in
            device?.proximityState = self.proximityState
        }
        return AnyCancellable {
            nc.removeObserver(battery)
            nc.removeObserver(prox)
        }
    }
}这确实迫使这些属性是非私有的。如果您愿意的话,可以通过传递WritableKeyPaths来解决这个问题。很多方法都能奏效。但它们都遵循抽象更新而不是模拟最终数据存储的模式。
https://stackoverflow.com/questions/66643065
复制相似问题