Search code examples
databaseswiftuimockingpreview

What's the best way to set preview data in SwiftUI which is normally handled by calling a database fetch?


My app has calls to a database in the .onAppear method of a view, but I want to use the preview feature of SwiftUI. Is there a way I can mock or disable this function in the preview window?

struct MyView: View {

    @State var dataArray: [Data] = [Data]()
     
    var body: some View {
        VStack { 
             ForEach(dataArray) { data in
                 Text(data.text) 
        }
        .onAppear {
            getData()
        }
    }

    private func getData() {
        someCallToDatabase() { returnedData in
            self.dataArray = returnedData
    }
}

struct MyView_Previews: PreviewProvider {
    
    static var previews: some View {
        MyView()        
    }
}

Basically I'd like to either pass the data in manually without it being overwritten, or somehow add an extension to my database caller class where it automatically sets some dummy data when it knows it's just for the preview.

Thanks a bunches, crunches!


Solution

  • By using some sort of dependency injection, you can make sure that there's a class/object responsible for doing the database calls that can be mocked. One version of this might be using @Envrionment to send in an object to handle the database calls:

    
    struct ApiData : Identifiable {
        var id = UUID()
        var text : String
    }
    
    protocol DatabaseManager {
        func someCallToDatabase(_ callback : ([ApiData]) -> Void)
    }
    
    class RealDatabaseManager : DatabaseManager {
        func someCallToDatabase(_ callback: ([ApiData]) -> Void) {
            callback([]) //do real database call here
        }
    }
    
    class MockDatabaseManager : DatabaseManager {
        func someCallToDatabase(_ callback: ([ApiData]) -> Void) {
            callback([])
        }
    }
    
    private struct DatabaseEnvironmentKey: EnvironmentKey {
        static let defaultValue : DatabaseManager = RealDatabaseManager()
    }
    
    extension EnvironmentValues {
        var databaseManager : DatabaseManager {
            get { self[DatabaseEnvironmentKey.self] }
            set { self[DatabaseEnvironmentKey.self] = newValue }
        }
    }
    
    struct MyView: View {
    
        @State var dataArray: [ApiData] = [ApiData]()
        @Environment(\.databaseManager) private var databaseManager : DatabaseManager
         
        var body: some View {
            VStack {
                 ForEach(dataArray) { data in
                     Text(data.text)
                 }
            }
            .onAppear {
                getData()
            }
        }
    
        func getData() {
            databaseManager.someCallToDatabase { returnedData in
                self.dataArray = returnedData
            }
        }
    }
    
    struct MyView_Previews: PreviewProvider {
        static var previews: some View {
            MyView().environment(\.databaseManager, MockDatabaseManager())
        }
    }
    
    

    Of course, you'll need to make sure that in your non-preview views, you're also passing the real database manager down the view hierarchy using .environment(...) in a parent view, so it'll involve some slight revision of your existing code.