SwiftUI - Bagaimana cara mengirimkan EnvironmentObject ke dalam View Model?

16

Saya mencari untuk membuat EnvironmentObject yang dapat diakses oleh View Model (bukan hanya tampilan).

Objek Lingkungan melacak data sesi aplikasi, misalnya login, token akses dll, data ini akan diteruskan ke model tampilan (atau kelas layanan di mana diperlukan) untuk memungkinkan panggilan API untuk meneruskan data dari Proyek Lingkungan ini.

Saya telah mencoba untuk meneruskan objek sesi ke penginisialisasi kelas model tampilan dari tampilan tetapi mendapatkan kesalahan.

bagaimana saya bisa mengakses / meneruskan EnvironmentObject ke model tampilan menggunakan SwiftUI?

Lihat tautan untuk menguji proyek: https://gofile.io/?c=vgHLVx

Michael
sumber
Mengapa tidak lulus viewmodel sebagai EO?
E.Coms
Tampaknya di atas, akan ada banyak model tampilan, unggahan yang saya tautkan hanyalah contoh sederhana
Michael
2
Saya tidak yakin mengapa pertanyaan ini dibatalkan, saya bertanya-tanya sama. Saya akan menjawab dengan apa yang telah saya lakukan, semoga orang lain dapat menemukan sesuatu yang lebih baik.
Michael Ozeryansky
2
@ E. Com Saya berharap EnvironmentObject secara umum menjadi satu objek. Saya tahu banyak pekerjaan, sepertinya bau kode untuk membuatnya dapat diakses secara global seperti itu.
Michael Ozeryansky
@Michael Apakah Anda menemukan solusi untuk ini?
Brett

Jawaban:

3

Saya memilih untuk tidak memiliki ViewModel. (Mungkin waktu untuk pola baru?)

Saya telah mengatur proyek saya dengan RootViewdan beberapa pandangan anak. Saya mengatur saya RootViewdengan Appobjek sebagai EnvironmentObject. Alih-alih ViewModel mengakses Model, semua pandangan saya mengakses kelas di App. Alih-alih ViewModel menentukan tata letak, hierarki tampilan menentukan tata letak. Dari melakukan ini dalam praktik untuk beberapa aplikasi, saya menemukan pandangan saya tetap kecil dan spesifik. Sebagai penyederhanaan berlebihan:

class App {
   @Published var user = User()

   let networkManager: NetworkManagerProtocol
   lazy var userService = UserService(networkManager: networkManager)

   init(networkManager: NetworkManagerProtocol) {
      self.networkManager = networkManager
   }

   convenience init() {
      self.init(networkManager: NetworkManager())
   }
}
struct RootView {
    @EnvironmentObject var app: App

    var body: some View {
        if !app.user.isLoggedIn {
            LoginView()
        } else {
            HomeView()
        }
    }
}
struct HomeView: View {
    @EnvironmentObject var app: App

    var body: some View {
       VStack {
          Text("User name: \(app.user.name)")
          Button(action: { app.userService.logout() }) {
             Text("Logout")
          }
       }
    }
}

Dalam preview saya, saya menginisialisasi MockAppyang merupakan subkelas dari App. MockApp menginisialisasi inisialisasi yang ditunjuk dengan objek Mocked. Di sini UserService tidak perlu diejek, tetapi sumber data (yaitu NetworkManagerProtocol) tidak.

struct HomeView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            HomeView()
                .environmentObject(MockApp() as App) // <- This is needed for EnvironmentObject to treat the MockApp as an App Type
        }
    }

}
Michael Ozeryansky
sumber
Hanya sebuah catatan: Saya pikir lebih baik untuk menghindari seperti rantai app.userService.logout(). userServiceharus bersifat pribadi dan hanya diakses dari dalam kelas aplikasi. Kode di atas akan terlihat seperti ini: Button(action: { app.logout() })dan fungsi logout kemudian akan memanggil langsung userService.logout().
pawello2222
@ pawello2222 Ini tidak lebih baik, itu hanya pola fasad tanpa manfaat, tetapi Anda dapat melakukan apa yang Anda inginkan.
Michael Ozeryansky
3

Anda seharusnya tidak. Ini adalah kesalahpahaman umum bahwa SwiftUI bekerja paling baik dengan MVVM.

MVVM tidak memiliki tempat di SwfitUI. Anda bertanya apakah Anda bisa mendorong persegi panjang ke

pas bentuk segitiga. Itu tidak cocok.

Mari kita mulai dengan beberapa fakta dan bekerja selangkah demi selangkah:

  1. ViewModel adalah model dalam MVVM.

  2. MVVM tidak mempertimbangkan tipe nilai (misalnya; tidak ada hal seperti itu di java) yang menjadi pertimbangan.

  3. Model tipe nilai (model tanpa negara) dianggap lebih aman daripada referensi

    tipe model (model with state) dalam arti immutability.

Sekarang, MVVM mengharuskan Anda untuk mengatur model sedemikian rupa sehingga setiap kali itu berubah, itu

pembaruan melihat dalam beberapa cara yang telah ditentukan sebelumnya. Ini dikenal sebagai penjilidan.

Tanpa ikatan, Anda tidak akan memiliki pemisahan kekhawatiran yang bagus, misalnya; refactoring keluar

model dan negara terkait dan menjaga mereka terpisah dari pandangan.

Ini adalah dua hal yang gagal dilakukan sebagian besar pengembang MVVM iOS:

  1. iOS tidak memiliki mekanisme "mengikat" dalam pengertian java tradisional.

    Beberapa hanya akan mengabaikan pengikatan, dan berpikir memanggil objek ViewModel

    secara otomatis memecahkan segalanya; beberapa akan memperkenalkan Rx berbasis KVO, dan

    mempersulit segalanya ketika MVVM seharusnya membuat segalanya lebih sederhana.

  2. Model dengan negara terlalu berbahaya

    karena MVVM terlalu menekankan pada ViewModel, terlalu sedikit pada manajemen negara

    dan disiplin umum dalam mengelola Kontrol; sebagian besar pengembang berakhir

    berpikir model dengan status yang digunakan untuk memperbarui tampilan dapat digunakan kembali dan

    dapat diuji .

    inilah mengapa Swift memperkenalkan tipe nilai sejak awal; model tanpa

    negara.

Sekarang untuk pertanyaan Anda: Anda bertanya apakah ViewModel Anda dapat memiliki akses ke EnvironmentObject (EO)?

Anda seharusnya tidak. Karena dalam SwiftUI model yang sesuai dengan Lihat secara otomatis miliki

referensi ke EO. Misalnya;

struct Model: View {
    @EnvironmentObject state: State
    // automatic binding in body
    var body: some View {...}
}

Saya harap orang-orang dapat menghargai betapa kompaknya SDK dirancang.

Di SwiftUI, MVVM otomatis . Tidak perlu untuk objek ViewModel yang terpisah

yang secara manual mengikat untuk melihat yang memerlukan referensi EO diteruskan ke sana.

Kode di atas adalah MVVM. Misalnya; model dengan binding to view.

Tetapi karena model adalah tipe nilai, jadi alih-alih refactoring keluar model dan nyatakan sebagai

lihat model, Anda menolak kontrol (dalam ekstensi protokol, misalnya).

Ini adalah SDK resmi yang mengadaptasi pola desain ke fitur bahasa, bukan hanya

menegakkannya. Substansi melebihi bentuk.

Lihatlah solusi Anda, Anda harus menggunakan singleton yang pada dasarnya global. Kamu

harus tahu betapa berbahayanya mengakses global di mana saja tanpa perlindungan

kekekalan, yang tidak Anda miliki karena Anda harus menggunakan model tipe referensi!

TL; DR

Anda tidak melakukan MVVM dengan cara java di SwiftUI. Dan cara Swift-y untuk melakukannya tidak perlu

untuk melakukannya, itu sudah built-in.

Semoga lebih banyak pengembang melihat ini karena ini sepertinya pertanyaan yang populer.

Jim lai
sumber
1

Di bawah ini disediakan pendekatan yang berfungsi untuk saya. Diuji dengan banyak solusi dimulai dengan Xcode 11.1.

Masalahnya berasal dari cara EnvironmentObject disuntikkan dalam pandangan, skema umum

SomeView().environmentObject(SomeEO())

yaitu, pada tampilan pertama yang dibuat, pada objek lingkungan kedua dibuat, pada objek lingkungan ketiga disuntikkan ke dalam tampilan

Jadi jika saya perlu membuat / menata model tampilan dalam view constructor objek lingkungan belum ada di sana.

Solusi: pisahkan semuanya dan gunakan injeksi ketergantungan eksplisit

Berikut ini tampilannya dalam kode (skema umum)

// somewhere, say, in SceneDelegate

let someEO = SomeEO()                            // create environment object
let someVM = SomeVM(eo: someEO)                  // create view model
let someView = SomeView(vm: someVM)              // create view 
                   .environmentObject(someEO)

Tidak ada trade-off di sini, karena ViewModel dan EnvironmentObject adalah, dengan desain, tipe referensi (sebenarnya, ObservableObject), jadi saya lulus di sini dan hanya ada referensi (alias pointer).

class SomeEO: ObservableObject {
}

class BaseVM: ObservableObject {
    let eo: SomeEO
    init(eo: SomeEO) {
       self.eo = eo
    }
}

class SomeVM: BaseVM {
}

class ChildVM: BaseVM {
}

struct SomeView: View {
    @EnvironmentObject var eo: SomeEO
    @ObservedObject var vm: SomeVM

    init(vm: SomeVM) {
       self.vm = vm
    }

    var body: some View {
        // environment object will be injected automatically if declared inside ChildView
        ChildView(vm: ChildVM(eo: self.eo)) 
    }
}

struct ChildView: View {
    @EnvironmentObject var eo: SomeEO
    @ObservedObject var vm: ChildVM

    init(vm: ChildVM) {
       self.vm = vm
    }

    var body: some View {
        Text("Just demo stub")
    }
}
Asperi
sumber