我一直在试验使用的 MVVM 模型,SwiftUI但有些东西我还不太明白。
SwiftUI使用@ObservableObject/@ObservedObject检测视图模型中触发body属性重新计算以更新视图的更改。
在 MVVM 模型中,这是视图和视图模型之间的通信。我不太明白的是模型和视图模型是如何通信的。
当模型发生变化时,视图模型应该如何知道这一点?我想到了手动使用新Combine框架在模型内部创建视图模型可以订阅的发布者。
但是,我创建了一个简单的示例,我认为该示例使这种方法非常乏味。有一个模型称为Game保存Game.Character对象数组。一个角色有一个strength可以改变的属性。
那么如果一个视图模型改变strength了一个角色的那个属性呢?为了检测这种变化,模型必须订阅游戏中的每一个角色(可能还有很多其他的东西)。是不是有点过分了?或者有很多发布者和订阅者是正常的吗?
还是我的示例没有正确遵循 MVVM?我的视图模型不应该将实际模型game作为属性吗?如果是这样,什么是更好的方法?
// My Model
class Game {
class Character {
let name: String
var strength: Int
init(name: String, strength: Int) {
self.name = name
self.strength = strength
}
}
var characters: [Character]
init(characters: [Character]) {
self.characters = characters
}
}
// ...
// My view model
class ViewModel: …Run Code Online (Sandbox Code Playgroud) 通常我们可以didSet在 swift中使用来监视变量的更新。但它不适用于@Binding变量。例如,我有以下代码:
@Binding var text {
didSet {
......
}
}
Run Code Online (Sandbox Code Playgroud)
但是didSet从来没有被调用过。知道吗?谢谢。
在我的 SwiftUI 应用程序中,每次值更改时我都需要从 ObservedObject 获取数据。我知道我们可以用 .onReceive 做到这一点?我不太了解 Apple 的文档。我不知道我怎么能做到这一点。
我的代码:
import SwiftUI
import CoreLocation
struct Compass: View {
@StateObject var location = LocationManager()
@State private var angle: CGFloat = 0
var body: some View {
VStack {
Image("arrow")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 300)
.modifier(RotationEffect(angle: -CGFloat(self.angle.degreesToRadians)))
.onReceive(location, perform: {
withAnimation(.easeInOut(duration: 1.0)) {
self.angle = self.location.heading
}
})
Text(String(self.location.heading.degreesToRadians))
.font(.system(size: 20))
.fontWeight(.light)
.padding(.top, 15)
}
}
}
struct RotationEffect: GeometryEffect {
var angle: CGFloat
var animatableData: CGFloat {
get { …Run Code Online (Sandbox Code Playgroud) 我试图找出构建绑定到 UserDefaults 的简单设置屏幕的最佳方法。
基本上,我有一个 Toggle 并且我想要:
我已经观看了许多 SwiftUI WWDC 会议,但我仍然不确定应该如何使用 Combine 和 SwiftUI 中提供的不同工具来设置所有内容。我目前的想法是我应该使用 BindableObject 以便我可以使用 hat 来封装许多不同的设置。
我想我很接近,因为它几乎按预期工作,但行为不一致。
当我在设备上构建和运行它时,我打开它并打开 Toggle,然后如果我向上和向下滚动视图一点开关切换回关闭(好像它实际上没有保存 UserDefaults 中的值)。
但是,如果我打开开关,离开应用程序,然后再回来,它仍然打开,就像它记住了设置一样。
有什么建议?我发布此内容是希望它能帮助其他不熟悉 SwiftUI 和 Combine 的人,因为我找不到关于此主题的任何类似问题。
import SwiftUI
import Combine
struct ContentView : View {
@ObjectBinding var settingsStore = SettingsStore()
var body: some View {
NavigationView {
Form {
Toggle(isOn: $settingsStore.settingActivated) {
Text("Setting Activated")
}
}
}.navigationBarTitle(Text("Settings"))
}
}
class SettingsStore: BindableObject {
var didChange …Run Code Online (Sandbox Code Playgroud) 在 Swift 中,您可以在初始化时覆盖的结构上定义默认值:
struct myStruct {
var a: Int = 1
}
var instance1 = myStruct() // instance1.a -> 1
var instance2 = myStruct(a: 10) // instance2.a -> 10
Run Code Online (Sandbox Code Playgroud)
但是,当我尝试将其应用于 SwiftUI 视图中的 Bindings 时,出现错误:
struct MyView: View {
@Binding var a: Bool = Binding.constant(true)
var body: some View {
Text("MyView")
}
}
Run Code Online (Sandbox Code Playgroud)
Argument labels '(wrappedValue:)' do not match any available overloads
Run Code Online (Sandbox Code Playgroud)
我想创建一个默认使用常量布尔值但可以被“真实”绑定覆盖的视图:
Argument labels '(wrappedValue:)' do not match any available overloads
Run Code Online (Sandbox Code Playgroud)
是否可以在 SwiftUI 中为绑定定义这样的默认值?
尝试编译以下代码时:
class LoginViewModel: ObservableObject, Identifiable {
@Published var mailAdress: String = ""
@Published var password: String = ""
@Published var showRegister = false
@Published var showPasswordReset = false
private let applicationStore: ApplicationStore
init(applicationStore: ApplicationStore) {
self.applicationStore = applicationStore
}
var passwordResetView: some View {
PasswordResetView(isPresented: $showPasswordReset) // This is where the error happens
}
}
Run Code Online (Sandbox Code Playgroud)
其中 PasswordResetView 看起来像这样:
struct PasswordResetView: View {
@Binding var isPresented: Bool
@State var mailAddress: String = ""
var body: some View {
EmptyView()
}
} …Run Code Online (Sandbox Code Playgroud) 在遵循当前 SwiftUI 语法使用 @Published 属性包装器时,似乎很难定义包含带有 @Published 的属性的协议,或者我肯定需要帮助:)
当我在 View 和它的 ViewModel 之间实现依赖注入时,我需要定义一个 ViewModelProtocol 以便注入模拟数据以轻松预览。
这是我第一次尝试的,
protocol PersonViewModelProtocol {
@Published var person: Person
}
Run Code Online (Sandbox Code Playgroud)
我得到“在协议中声明的属性‘人’不能有包装器”。
然后我尝试了这个,
protocol PersonViewModelProtocol {
var $person: Published
}
Run Code Online (Sandbox Code Playgroud)
显然没有用,因为 '$' 是保留的。
我希望有一种方法可以在 View 和它的 ViewModel 之间建立一个协议,并利用优雅的 @Published 语法。非常感谢。
我收到上述错误,无法弄清楚如何解决它。我有一个包含布尔值的对象数组,需要为每个布尔值显示一个切换。
下面是代码。
class Item: Identifiable {
var id: String
var label: String
var isOn: Bool
}
class Service: ObservableObject {
var didChange = PassthroughSubject<Void, Never>()
var items: [Item] {
didSet {
didChange.send(())
}
}
}
struct MyView: View {
@ObservedObject var service: Service
var body: some View {
List {
ForEach(service.items, id: \.self) { (item: Binding<Item>) in
Section(header: Text(item.label)) { // Error: Initializer 'init(_:)' requires that 'Binding<String>' conform to 'StringProtocol'
Toggle(isOn: item.isOn) {
Text("isOn")
}
}
}
}
.listStyle(GroupedListStyle())
} …Run Code Online (Sandbox Code Playgroud) 所以我正在研究结合,这个问题就出现了。
使用CurrentValueSubject(并使用设置其值currentValueSubject.value)或使用 a@Published var并使用 a访问其发布者之间有什么真正的区别$吗?我的意思是我知道一个返回 aSubject而不是 a Publisher,但我能找到的唯一真正的区别是它CurrentValueSubject更有用,因为你可以在协议上声明它。
我真的不明白@Published如果我们可以使用它怎么会有用PassthroughSubject,我在这里遗漏了什么吗?
请注意,这是使用 UIKit,它可能对 SwiftUI 有其他用途。
谢谢你。
我在使用 iOS Combine 框架时遇到了一些心理障碍。
我正在将一些代码从“手动”从远程 API 获取转换为使用组合。基本上,API 是 SQL 和 REST(实际上是 Salesforce,但这与问题无关)。代码用来做的是调用一个接受完成处理程序的 REST 查询方法。我正在做的是用结合未来到处替换它。到现在为止还挺好。
当以下场景发生时,问题就出现了(并且经常发生):
我们执行 REST 查询并返回一组“对象”。
但是这些“对象”并没有完全填充。它们中的每一个都需要来自某个相关对象的附加数据。因此,对于每个“对象”,我们使用来自该“对象”的信息进行另一个 REST 查询,从而为我们提供另一个“对象”数组。
这可能允许也可能不允许我们完成第一个“对象”的填充——否则,我们可能必须使用来自第二个“对象”中的每个“对象”的信息进行另一个REST 查询,依此类推。
结果是很多这样结构的代码(这是伪代码):
func fetchObjects(completion: @escaping ([Object] -> Void) {
let restQuery = ...
RESTClient.performQuery(restQuery) { results in
let partialObjects = results.map { ... }
let group = DispatchGroup()
for partialObject in partialObjects {
let restQuery = ... // something based on partialObject
group.enter()
RESTClient.performQuery(restQuery) { results in
group.leave()
let partialObjects2 = results.map …Run Code Online (Sandbox Code Playgroud)