アプリのコードの見栄えを良くするために、ロジックを含むすべてのビューに対してViewModelを作成します。
通常のViewModelは次のようになります。
class SomeViewModel: ObservableObject {
@Published var state = 1
// Logic and calls of Business Logic goes here
}
次のように使用されます:
struct SomeView: View {
@ObservedObject var viewModel = SomeViewModel()
var body: some View {
// Code to read and write the State goes here
}
}
これは、ビューの親が更新されていない場合に正常に機能します。親の状態が変化すると、このビューは再描画されます(宣言型フレームワークではかなり正常です)。 しかしまた、ViewModelは再作成され、後で状態を保持しません。他のフレームワーク(例:Flutter)と比較すると、これは異常です。
私の意見では、ViewModelはそのままであるか、Stateが持続するはずです。
ViewModelを@State
プロパティで置き換え、int
(この例では)を直接使用すると、永続化されたままになり、再作成されません:
struct SomeView: View {
@State var state = 1
var body: some View {
// Code to read and write the State goes here
}
}
これは明らかに、より複雑な州では機能しません。また、@State
のクラス(ViewModelなど)を設定すると、期待どおりに機能しなくなります。
@State
の@ObservedObject
Propertywrapperを複製する方法はありますか?通常、内部ビューでViewModelを作成することは悪い習慣ですが、この動作はNavigationLinkまたはSheetを使用して再現できます。
セル自体に多くのロジックが含まれている非常に複雑なTableViewを考える場合、ParentsViewModelで状態を保持してバインディングを操作することが役に立たない場合があります。
個々のケースには常に回避策がありますが、ViewModelを再作成しない方がはるかに簡単だと思います。
この問題については、非常に具体的なユースケースについての質問がたくさんあります。ここでは、カスタムソリューションについて深く掘り下げることなく、一般的な問題について説明します。
データベース、API、またはキャッシュからのリストのように、状態が変化するParentViewがある場合(単純なものについて考えてください)。 NavigationLink
を介して、データを変更できる詳細ページにアクセスできます。データを変更することにより、リアクティブ/宣言型パターンはListViewも更新するように指示します。これにより、NavigationLink
が「再描画」され、ViewModelが再作成されます。
ParentView/ParentViewのViewModelにViewModelを格納できることはわかっていますが、これはIMOを実行する方法としては間違っています。サブスクリプションが破棄または再作成されるため、副作用が発生する可能性があります。
最後に、アップルが提供するソリューションがあります:@StateObject
。
@ObservedObject
を@StateObject
に置き換えることで、最初の投稿で述べたすべてが機能します。
残念ながら、これはiOS 14以降でのみ利用できます。
これはXcode 12 Betaからの私のコードです(2020年6月23日公開)
struct ContentView: View {
@State var title = 0
var body: some View {
NavigationView {
VStack {
Button("Test") {
self.title = Int.random(in: 0...1000)
}
TestView1()
TestView2()
}
.navigationTitle("\(self.title)")
}
}
}
struct TestView1: View {
@ObservedObject var model = ViewModel()
var body: some View {
VStack {
Button("Test1: \(self.model.title)") {
self.model.title += 1
}
}
}
}
class ViewModel: ObservableObject {
@Published var title = 0
}
struct TestView2: View {
@StateObject var model = ViewModel()
var body: some View {
VStack {
Button("StateObject: \(self.model.title)") {
self.model.title += 1
}
}
}
}
ご覧のとおり、StateObject
は、ObservedObject
がリセットされている間、親ビューの再描画時に値を保持します。
私はあなたに同意します。これはSwiftUIの多くの主要な問題の1つだと思います。これが私が自分でやっていることです。
struct MyView: View {
@State var viewModel = MyViewModel()
var body : some View {
MyViewImpl(viewModel: viewModel)
}
}
fileprivate MyViewImpl : View {
@ObservedObject var viewModel : MyViewModel
var body : some View {
...
}
}
ビューモデルを適切に構築するか、または渡すことができます。これにより、再構築全体でObservableObjectを維持するビューが得られます。
毎回ViewModelを再作成しない方法はありますか?
はい、ViewModelインスタンスoutside of SomeView
を保持し、コンストラクタを介して注入します
struct SomeView: View {
@ObservedObject var viewModel: SomeViewModel // << only declaration
@ObservedObjectの@State Propertywrapperを複製する方法はありますか?
必要ありません。 @ObservedObject
is-aすでにDynamicProperty
同様に@State
なぜ@Stateは再描画で国家を維持しているのですか?
つまり、ストレージを保持するためです。ラップされた値、ビューのoutside。 (したがって、上記の最初をもう一度参照してください)
PassThroughSubject
クラスでカスタムObservableObject
を提供する必要があります。このコードを見てください:
_//
// Created by Франчук Андрей on 08.05.2020.
// Copyright © 2020 Франчук Андрей. All rights reserved.
//
import SwiftUI
import Combine
struct TextChanger{
var textChanged = PassthroughSubject<String,Never>()
public func changeText(newValue: String){
textChanged.send(newValue)
}
}
class ComplexState: ObservableObject{
var objectWillChange = ObservableObjectPublisher()
let textChangeListener = TextChanger()
var text: String = ""
{
willSet{
objectWillChange.send()
self.textChangeListener.changeText(newValue: newValue)
}
}
}
struct CustomState: View {
@State private var text: String = ""
let textChangeListener: TextChanger
init(textChangeListener: TextChanger){
self.textChangeListener = textChangeListener
print("did init")
}
var body: some View {
Text(text)
.onReceive(textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomStateContainer: View {
//@ObservedObject var state = ComplexState()
var state = ComplexState()
var body: some View {
VStack{
HStack{
Text("custom state View: ")
CustomState(textChangeListener: state.textChangeListener)
}
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
HStack{
Text("text input: ")
TextInput().environmentObject(state)
}
}
}
}
struct TextInput: View {
@EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: $state.text)
}
}
struct CustomState_Previews: PreviewProvider {
static var previews: some View {
return CustomStateContainer()
}
}
_
最初に、TextChanger
を使用して、CustomState
ビューの.onReceive(...)
に_.text
_の新しい値を渡します。この場合のonReceive
はPassthroughSubject
ではなくObservableObjectPublisher
を取得することに注意してください。最後のケースでは、_Publisher.Output
_には_perform: closure
_のみが含まれ、NewValueは含まれません。その場合、_state.text
_は古い値になります。
次に、ComplexState
クラスを見てください。テキストの変更がサブスクライバーに手動で通知を送信するようにobjectWillChange
プロパティを作成しました。 _@Published
_ラッパーとほぼ同じです。しかし、テキストを変更すると、両方、およびobjectWillChange.send()
とtextChanged.send(newValue)
の両方が送信されます。これにより、状態の変化に対応する方法を正確にView
で選択できるようになります。通常の動作が必要な場合は、状態をCustomStateContainer
ビューの_@ObservedObject
_ラッパーに入れるだけです。次に、すべてのビューを再作成し、このセクションでも更新された値を取得します。
_HStack{
Text("ordinary Text View: ")
Text(state.text)
}
_
それらすべてを再作成したくない場合は、@ ObservedObjectを削除してください。通常のテキストビューは更新を停止しますが、CustomStateは停止します。再作成なし。
更新:より詳細な制御が必要な場合は、値を変更するときに、その変更について誰に通知するかを決定できます。より複雑なコードを確認してください:
_//
//
// Created by Франчук Андрей on 08.05.2020.
// Copyright © 2020 Франчук Андрей. All rights reserved.
//
import SwiftUI
import Combine
struct TextChanger{
// var objectWillChange: ObservableObjectPublisher
// @Published
var textChanged = PassthroughSubject<String,Never>()
public func changeText(newValue: String){
textChanged.send(newValue)
}
}
class ComplexState: ObservableObject{
var onlyPassthroughSend = false
var objectWillChange = ObservableObjectPublisher()
let textChangeListener = TextChanger()
var text: String = ""
{
willSet{
if !onlyPassthroughSend{
objectWillChange.send()
}
self.textChangeListener.changeText(newValue: newValue)
}
}
}
struct CustomState: View {
@State private var text: String = ""
let textChangeListener: TextChanger
init(textChangeListener: TextChanger){
self.textChangeListener = textChangeListener
print("did init")
}
var body: some View {
Text(text)
.onReceive(textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomStateContainer: View {
//var state = ComplexState()
@ObservedObject var state = ComplexState()
var body: some View {
VStack{
HStack{
Text("custom state View: ")
CustomState(textChangeListener: state.textChangeListener)
}
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
HStack{
Text("text input with full state update: ")
TextInput().environmentObject(state)
}
HStack{
Text("text input with no full state update: ")
TextInputNoUpdate().environmentObject(state)
}
}
}
}
struct TextInputNoUpdate: View {
@EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: Binding( get: {self.state.text},
set: {newValue in
self.state.onlyPassthroughSend.toggle()
self.state.text = newValue
self.state.onlyPassthroughSend.toggle()
}
))
}
}
struct TextInput: View {
@State private var text: String = ""
@EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: Binding(
get: {self.text},
set: {newValue in
self.state.text = newValue
// self.text = newValue
}
))
.onAppear(){
self.text = self.state.text
}.onReceive(state.textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomState_Previews: PreviewProvider {
static var previews: some View {
return CustomStateContainer()
}
}
_
ObjectWillChangeのブロードキャストを停止する手動バインディングを作成しました。ただし、同期を維持するには、この値を変更するすべての場所で新しい値を取得する必要があります。そのため、TextInputも変更しました。
それが必要ですか?