SwiftUI - วิธีการส่ง EnvironmentObject เป็น View Model?


16

ฉันต้องการสร้าง EnvironmentObject ที่สามารถเข้าถึงได้โดย View Model (ไม่ใช่เฉพาะมุมมอง)

วัตถุสภาพแวดล้อมติดตามข้อมูลเซสชันแอปพลิเคชันเช่น logIn, โทเค็นการเข้าถึง ฯลฯ ข้อมูลนี้จะถูกส่งผ่านไปยังมุมมองรูปแบบ (หรือคลาสบริการที่จำเป็น) เพื่อให้สามารถเรียกใช้ API เพื่อส่งผ่านข้อมูลจาก EnvironmentObjects นี้

ฉันพยายามส่งผ่านวัตถุเซสชันไปยังผู้เริ่มต้นของคลาสโมเดลมุมมองจากมุมมอง แต่ได้รับข้อผิดพลาด

ฉันจะเข้าถึง / ส่ง EnvironmentObject ไปยังโมเดลมุมมองโดยใช้ SwiftUI ได้อย่างไร

ดูลิงค์เพื่อทดสอบโครงการ: https://gofile.io/?c=vgHLVx


ทำไมไม่ส่ง viewmodel เป็น EO?
E.Coms

ดูเหมือนว่าด้านบนจะมีโมเดลหลายมุมมองการอัปโหลดที่ฉันเชื่อมโยงเป็นเพียงตัวอย่างง่าย ๆ
Michael

2
ฉันไม่แน่ใจว่าทำไมคำถามนี้จึงลดลงฉันสงสัยเหมือนกัน ฉันจะตอบกับสิ่งที่ฉันทำหวังว่าคนอื่นอาจจะคิดอะไรดีกว่านี้
Michael Ozeryansky

2
@ E.Coms ฉันคาดว่า EnvironmentObject โดยทั่วไปจะเป็นหนึ่งวัตถุ ฉันรู้ว่ามีหลายงานดูเหมือนว่าจะเป็นกลิ่นรหัสที่ทำให้พวกเขาสามารถเข้าถึงได้ทั่วโลกเช่นนั้น
Michael Ozeryansky

@Michael คุณยังหาวิธีแก้ปัญหานี้หรือไม่?
Brett

คำตอบ:


3

ฉันเลือกที่จะไม่มี ViewModel (อาจถึงเวลาสำหรับรูปแบบใหม่หรือไม่)

ฉันได้เซ็ตอัพโครงการด้วยRootViewมุมมองแบบลูกและแบบบางแบบ ฉันตั้งค่าของฉันRootViewด้วยAppวัตถุเป็น EnvironmentObject แทนที่จะเป็นโมเดลการเข้าถึงของ ViewModel มุมมองของฉันทั้งหมดจะเข้าถึงคลาสบนแอพ แทน ViewModel ที่กำหนดเค้าโครงมุมมองลำดับชั้นจะกำหนดเค้าโครง จากการดำเนินการนี้ในทางปฏิบัติสำหรับแอปไม่กี่ฉันพบว่ามุมมองของฉันมีขนาดเล็กและเฉพาะเจาะจง เป็นการง่ายกว่า:

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")
          }
       }
    }
}

ในตัวอย่างของฉันฉันเริ่มต้นMockAppซึ่งเป็น subclass Appของ MockApp เริ่มต้น initializers ที่กำหนดด้วยวัตถุ Mocked ที่นี่ UserService ไม่จำเป็นต้องถูกเยาะเย้ย แต่แหล่งข้อมูล (เช่น NetworkManagerProtocol) ทำ

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
        }
    }

}

เพียงแค่ทราบ: app.userService.logout()ผมคิดว่ามันจะดีกว่าที่จะหลีกเลี่ยงการผูกมัดเช่น userServiceควรเป็นแบบส่วนตัวและเข้าถึงได้จากภายในแอพเท่านั้น รหัสดังกล่าวควรมีลักษณะเช่นนี้และฟังก์ชั่นออกจากระบบแล้วจะโทรโดยตรงButton(action: { app.logout() }) userService.logout()
pawello2222

@ pawello2222 มันไม่ได้ดีกว่ามันเป็นเพียงรูปแบบด้านหน้าที่ไม่มีประโยชน์ แต่คุณสามารถทำได้ตามที่คุณต้องการ
Michael Ozeryansky

3

คุณไม่ควร มันเป็นความเข้าใจผิดทั่วไปที่ SwiftUI ทำงานได้ดีที่สุดกับ MVVM

MVVM ไม่มีสถานที่ใน SwfitUI คุณจะถามว่าถ้าคุณสามารถผลักสี่เหลี่ยมผืนผ้า

พอดีกับรูปสามเหลี่ยม มันไม่พอดี

เริ่มจากข้อเท็จจริงและงานทีละขั้นตอน:

  1. ViewModel เป็นรูปแบบใน MVVM

  2. MVVM ไม่นำค่าประเภท (เช่นไม่มีสิ่งใดใน java) มาพิจารณา

  3. รูปแบบประเภทค่า (รุ่นที่ไม่มีสถานะ) ถือว่าปลอดภัยกว่าการอ้างอิง

    type model (model with state) ในแง่ของความไม่เปลี่ยนแปลง

ตอนนี้ MVVM ต้องการให้คุณตั้งค่ารูปแบบในแบบที่ทุกครั้งที่มีการเปลี่ยนแปลง

มุมมองการปรับปรุงในบางวิธีที่กำหนดไว้ล่วงหน้า สิ่งนี้เรียกว่าการรวม

คุณจะไม่มีข้อกังวลที่ดีเช่น; refactoring out

รูปแบบและสถานะที่เกี่ยวข้องและทำให้พวกเขาแยกจากมุมมอง

นี่คือสองสิ่งที่นักพัฒนา iOS MVVM ส่วนใหญ่ล้มเหลว:

  1. iOS ไม่มีกลไก "ผูกมัด" ในความหมายของจาวาแบบดั้งเดิม

    บางคนก็ไม่สนใจการเชื่อมโยงและคิดว่าการเรียกใช้ ViewModel ของวัตถุ

    แก้ปัญหาทุกอย่างโดยอัตโนมัติ บางคนจะแนะนำ Rx ที่ใช้ KVO และ

    ซับซ้อนทุกอย่างเมื่อ MVVM ควรจะทำให้สิ่งต่าง ๆ ง่ายขึ้น

  2. โมเดลที่มีสถานะเป็นอันตรายเกินไป

    เนื่องจาก MVVM ให้ความสำคัญกับ ViewModel มากเกินไปน้อยเกินไปในการจัดการสถานะ

    และสาขาวิชาทั่วไปในการจัดการการควบคุม; นักพัฒนาส่วนใหญ่จะจบลง

    การคิดแบบจำลองที่มีสถานะที่ใช้ในการอัปเดตมุมมองนั้นสามารถนำมาใช้ซ้ำได้และ

    ทดสอบทดสอบ

    นี่คือสาเหตุที่ Swift แนะนำประเภทค่าในตอนแรก รูปแบบที่ไม่มี

    สถานะ.

ตอนนี้สำหรับคำถามของคุณ: คุณถามว่า ViewModel ของคุณสามารถเข้าถึง EnvironmentObject (EO) ได้หรือไม่?

คุณไม่ควร เพราะใน SwiftUI รุ่นที่สอดคล้องกับ View จะมีโดยอัตโนมัติ

อ้างอิงถึง EO เช่น;

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

ฉันหวังว่าผู้คนจะพึงพอใจกับการออกแบบ SDK ที่กะทัดรัด

ใน SwiftUI, MVVM เป็นอัตโนมัติ ไม่จำเป็นต้องมีวัตถุ ViewModel แยกต่างหาก

ที่ผูกไว้เพื่อดูด้วยตนเองซึ่งต้องมีการอ้างอิง EO ผ่านไป

รหัสข้างต้นคือ MVVM เช่น; โมเดลที่มีการเชื่อมโยงเพื่อดู

แต่เนื่องจากตัวแบบเป็นประเภทค่าดังนั้นแทนที่จะปรับเปลี่ยนโมเดลและสถานะเป็น

มุมมองรูปแบบคุณ refactor out control (ตัวอย่างเช่นในส่วนขยายโปรโตคอล)

นี่คือ SDK อย่างเป็นทางการที่ปรับรูปแบบการออกแบบให้เป็นภาษาไม่ใช่แค่

บังคับใช้มัน สารมากกว่าแบบฟอร์ม

ดูวิธีแก้ปัญหาของคุณคุณต้องใช้ซิงเกิลตันซึ่งเป็นพื้นโลก คุณ

ควรรู้ว่าการเข้าถึงทั่วโลกนั้นอันตรายแค่ไหนโดยไม่ได้รับการคุ้มครอง

เปลี่ยนไม่ได้ซึ่งคุณไม่ได้เพราะคุณต้องใช้รูปแบบประเภทอ้างอิง!

TL; DR

คุณไม่ได้ทำ MVVM ด้วยวิธีจาวาใน SwiftUI และวิธีที่รวดเร็วในการทำก็ไม่จำเป็น

ที่จะทำมันมีอยู่แล้วในตัว

หวังว่านักพัฒนาซอฟต์แวร์จะเห็นสิ่งนี้ตั้งแต่นี้ดูเหมือนจะเป็นคำถามยอดนิยม


1

วิธีการด้านล่างนี้มีให้สำหรับฉัน ทดสอบกับโซลูชันมากมายที่เริ่มต้นด้วย Xcode 11.1

ปัญหาเกิดจากวิธีการฉีด EnvironmentObject ในมุมมองสคีทั่วไป

SomeView().environmentObject(SomeEO())

เช่นที่มุมมองที่สร้างขึ้นครั้งแรกที่วัตถุสภาพแวดล้อมที่สร้างขึ้นที่สองที่วัตถุสภาพแวดล้อมที่สามฉีดเข้าไปในมุมมอง

ดังนั้นหากฉันต้องการสร้าง / ตั้งค่ามุมมองแบบจำลองในมุมมองตัวสร้างวัตถุสภาพแวดล้อมยังไม่ปรากฏ

การแก้ไข: แยกทุกอย่างออกจากกันและใช้การฉีดพึ่งพาอย่างชัดเจน

นี่คือลักษณะของโค้ด (schema ทั่วไป)

// 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)

ไม่มีการแลกเปลี่ยนใด ๆ ที่นี่เนื่องจาก ViewModel และ EnvironmentObject คือโดยการออกแบบประเภทการอ้างอิง (ที่จริงแล้วObservableObject) ดังนั้นฉันผ่านที่นี่และมีการอ้างอิงเท่านั้น (ตัวชี้ aka)

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")
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.