很抱歉出现了一些模糊的标题,但我不确定更合适的标题是什么。
让我先解释一下我的设置和我想要达到的目标。
我定义了一个名为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
复制相似问题