Giter VIP home page Giter VIP logo

Comments (13)

hmlongco avatar hmlongco commented on August 23, 2024 6

So here's one example using namespaces that I've used. The mock0, mock1, and mockN modes are used in many places in the app to select mocks that are empty, have only one element, and have many elements.

import SwiftUI

extension Resolver.Name {
    static let api = Self("api")
    static let mock0 = Self("mock0")
    static let mock1 = Self("mock1")
    static let mockN = Self("mockN")
    static var mode: Resolver.Name = .api
}

extension Resolver: ResolverRegistering {
    public static func registerAllServices() {
        register { resolve(name: .mode) as DataProviding }
        register(name: .api) { APIDataProvider() as DataProviding }
        register(name: .mock0) { Mock0DataProvider() as DataProviding }
        register(name: .mock1) { Mock1DataProvider() as DataProviding }
        register(name: .mockN) { MockNDataProvider() as DataProviding }
    }
}

protocol DataProviding {
    var text: String { get }
}

struct APIDataProvider: DataProviding {
    var text: String = "API"
}

struct Mock0DataProvider: DataProviding {
    var text: String = "MOCK0"
}

struct Mock1DataProvider: DataProviding {
    var text: String = "MOCK1"
}

struct MockNDataProvider: DataProviding {
    var text: String = "MOCKN"
}

class ContentViewModel: ObservableObject {
    @Injected var data: DataProviding
    var text: String { "Hello, \(data.text) world!" }
}

struct ContentView: View {
    @StateObject var viewModel = ContentViewModel()
    var body: some View {
        Text(viewModel.text)
            .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Resolver.Name.mode = .mock1
        return ContentView()
    }
}

from resolver.

Prince2k3 avatar Prince2k3 commented on August 23, 2024 4

In the documentation. There's a section called custom containers. Within there is another section called Party Tricks. It was there I used the truck to do what I needed to do for Swift previews that needed Resolver. I made a mock version and swapped before the moment previews would perform the resolve @sthwicked

from resolver.

hmlongco avatar hmlongco commented on August 23, 2024 4

Plus another example using mock containers.

extension Resolver {
    static var mock: Resolver!
    static func setupMockMode() {
        Resolver.mock = Resolver(parent: .main)
        Resolver.root = .mock
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Resolver.setupMockMode()
        Resolver.mock.register { MockNDataProvider() as DataProviding }
        return ContentView()
    }
}

Using the mock container in unit tests is also straightforward.

import XCTest

@testable import ResolverSwiftUI

class ResolverSwiftUITests: XCTestCase {
    
    override func setUp() {
        Resolver.setupMockMode()
    }

    func testExample0() throws {
        Resolver.mock.register { Mock0DataProvider() as DataProviding }
        let viewModel = ContentViewModel()
        XCTAssert(viewModel.text == "Hello, MOCK0 world!")
    }

    func testExample1() throws {
        Resolver.mock.register { Mock1DataProvider() as DataProviding }
        let viewModel = ContentViewModel()
        XCTAssert(viewModel.text == "Hello, MOCK1 world!")
    }

    func testExampleN() throws {
        Resolver.mock.register { MockNDataProvider() as DataProviding }
        let viewModel = ContentViewModel()
        XCTAssert(viewModel.text == "Hello, MOCKN world!")
    }

}

from resolver.

oleiade avatar oleiade commented on August 23, 2024 1

Hi @hmlongco just a quick note from me to thank you! As an engineer relatively new to iOS dev, I've been trying to figure out how to do this for literally months. Your examples are extremely useful šŸ™šŸ» and I wanted to make sure to express my gratitude šŸŽ‰

I would concur with @Prince2k3, it would be worth adding this example to the documentation in my opinion; it is a quite useful, and practical example of how to integrate Resolver with SwiftUI.

Cheers šŸ¦•

from resolver.

Prince2k3 avatar Prince2k3 commented on August 23, 2024

Came up with something that kind of works.

from resolver.

yjosephides avatar yjosephides commented on August 23, 2024

@Prince2k3 Can you share your solution?

from resolver.

yjosephides avatar yjosephides commented on August 23, 2024

@Prince2k3 Appeciate it! Thanx

from resolver.

shawnkoh avatar shawnkoh commented on August 23, 2024

Hi @Prince2k3 sorry to revive this issue, but could you elaborate on how you swapped the container right before the previews resolved?

I tried

  • setting Resolver.root = .mock in the PreviewPRovider's init()
  • setting Resolver.root = .mock in the ViewModel's init
  • setting Resolver.root = .mock in the App's init

Unfortunately none of them update the SwiftUI Preview.
Swapping the root of Resolver when running the app on my device works though.

import SwiftUI
import Resolver
import Combine

final class GoalPickerViewModel: ObservableObject {
    @LazyInjected private var goalService: GoalService
    @LazyInjected private var alertService: AlertService

    @Published private(set) var untrackedGoals: [Goal] = []

    private var subscribers = Set<AnyCancellable>()

    init() {
        Resolver.root = .mock
        goalService.goalsPublisher
            .combineLatest(goalService.goalTrackersPublisher)
            .map { goals, goalTrackers in
                goals.filter { goalTrackers[$0.id] == nil }
            }
            .receive(on: DispatchQueue.main)
            .sink { [weak self] in self?.untrackedGoals = $0 }
            .store(in: &subscribers)
    }

    func trackGoal(_ goal: Goal) {
        goalService.trackGoal(goal)
            .errorHandled(by: alertService)
    }
}

struct GoalPicker: View {
    @StateObject private var viewModel = GoalPickerViewModel()

    var body: some View {
        ScrollView {
            LazyVStack(alignment: .leading) {
                ForEach(viewModel.untrackedGoals) { goal in
                    GoalCard(goal: goal)
                        .onTap { viewModel.trackGoal(goal) }
                        .cardButtonStyle(.modalCard)
                        .disabled(!goal.isTrackable)
                }
            }
        }
    }
}

struct GoalPicker_Previews: PreviewProvider {
    static var previews: some View {
        GoalPicker()
            .preferredColorScheme(.dark)
    }
}

from resolver.

Prince2k3 avatar Prince2k3 commented on August 23, 2024

I did the change, setting it to a mock type, inside the previews getter. That worked for me

from resolver.

Prince2k3 avatar Prince2k3 commented on August 23, 2024

Also Iā€™m no longer using Resolver all over my code like this. Iā€™m went with a more composable setup. I use Resolver at the top (app) level while everything else in mostly init injection. This to me makes it easier to test previews versus what was done before with resolver.

Another note: i recommended not using the property wrapper for injections in your view models. As it makes it hard to test and you lose control of how you inject data.

from resolver.

hmlongco avatar hmlongco commented on August 23, 2024

@Prince2k3 I posted several examples above. Note that I disagree (rather strongly in fact) in regard to your comment regarding using @injected in your viewModels and elsewhere in the stack.

As the test example shows, it's not hard to mock services for unit tests, and in fact using dependency injection let's you reach down deeper into the stack and swap out services that would be hard to change if you were using constructor injection and object A depended on B that depended on C that depended on D.

If "D" in this case was a network layer, you might want to swap it out for one that pulled mock json data from a directory. Resolver makes it simple to reregister D, then construct the viewModel which would then inject everything needed to build it.

from resolver.

Prince2k3 avatar Prince2k3 commented on August 23, 2024

Hey @hmlongco. It is my bad on adding my opinion on the issue and also my bad on word chosen. (Probably should have just @ you instead). Anyway glad you are here to help.

Can I ask if you can add this to the Readme? These are great solution to getting SwiftUI previews working with Resolver.

from resolver.

ptrkstr avatar ptrkstr commented on August 23, 2024

I agree with @Prince2k3 regarding not using the @Injected property wrappers but I disagree with @Prince2k3's reason why.
Due to the way property wrappers are implemented, @Injected cannot be used if you rely on using injected functionality in an initializer whilst also initializing properties in that initializer.
For that reason, if you want your method of injecting to be consistent between all your classes and you will be needing to initialize properties in the initializer, you cannot use @Injected.
An additional added benefit of not using @Injected is you can make your dependency be immutable via let (unless @hmlongco makes/has made an immutable version of @Injected).

See the following example demonstrating this:

protocol PersonFormatterType {
    func string(from person: Person) -> String
}

struct PersonFormatter: PersonFormatterType {
    func string(from person: Person) -> String {
        "\(person.name) is a great person"
    }
}

struct Person {
    let name: String
}

struct PersonView: View {
    
    @ObservedObject var viewModel: PersonViewModel
    
    init(person: Person) {
        viewModel = .init(person: person)
    }
    
    var body: some View {
        Text(viewModel.text)
            .padding()
    }
}

class PersonViewModel: ObservableObject {
    
    // THIS WON'T WORK: 'self' used in property access 'formatter' before all stored properties are initialized
    @Injected private var formatter: PersonFormatterType
    
    // THIS WILL WORK
    private let formatter: PersonFormatterType = Resolver.main.resolve()

    let text: String

    init(person: Person) {
        text = formatter.string(from: person)
    }
}

from resolver.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    šŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. šŸ“ŠšŸ“ˆšŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ā¤ļø Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.