SwiftUI:使用ViewModel中的数据进行预览

dy2hfwbg  于 2022-12-17  发布在  Swift
关注(0)|答案(5)|浏览(214)

我从正在从Web加载数据的viewModel加载数据。问题:我想将一些预览示例数据设置为在预览窗口中具有内容。由于我未提供数据,因此当前预览包含空列表。
我怎样才能做到这一点?

struct MovieListView: View {

    @ObservedObject var viewModel = MovieViewModel()

    var body: some View {
       List{
        ForEach(viewModel.movies) { movie in
                MovieRow(movie: movie)
                    .listRowInsets(EdgeInsets())
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        MovieListView()
    }
}

class MovieViewModel: ObservableObject{

    private let provider = NetworkManager()

    @Published var movies = [Movie]()

    init() {
       loadNewMovies()
    }

    func loadNewMovies(){
         provider.getNewMovies(page: 1) {[weak self] movies in
                   print("\(movies.count) new movies loaded")
                   self?.movies.removeAll()
            self?.movies.append(contentsOf: movies)}
    }
}
rdrgkggo

rdrgkggo1#

下面是一种可能的方法(基于视图模型成员的依赖注入,而不是紧耦合)

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        // create Movie to be previewed inline, say from bundled data
        MovieListView(viewModel: MovieViewModel(provider: nil, movies: [Movie(...)]))
    }
}

class MovieViewModel: ObservableObject {

    private var provider: NetworkManager?

    @Published var movies: [Movie]
    
    // same as before by default, but allows to modify if/when needed explicitly
    init(provider: NetworkManager? = NetworkManager(), movies: [Movie] = []) {
        self.provider = provider
        self.movies = movies

        loadNewMovies()
    }

    func loadNewMovies(){
         provider?.getNewMovies(page: 1) {[weak self] movies in
                print("\(movies.count) new movies loaded")
                self?.movies.removeAll()
                self?.movies.append(contentsOf: movies)
        }
    }
}
g6ll5ycj

g6ll5ycj2#

这个问题是在@StateObject在WWDC 2020上推出之前写的。我相信现在你会想使用@StateObject而不是@ObservedObject,因为否则你的视图模型可能会被重新初始化很多次(在这种情况下会导致多次网络调用)。
我想做和OP完全一样的事情,但是用的是@StateObject。这是我的解决方案,不依赖于任何构建配置。

struct MovieListView: View {

    @StateObject var viewModel = MovieViewModel()

    var body: some View {
        MovieListViewInternal(viewModel: viewModel)
    }
}

private struct MovieListViewInternal<ViewModel: MovieViewModelable>: View {

    @ObservedObject var viewModel: ViewModel

    var body: some View {
       List {
           ForEach(viewModel.movies) { movie in
               MovieRow(movie: movie)
           }
       }
       .onAppear {
           viewModel.fetchMovieRatings()
       }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        MovieListViewInternal(viewModel: PreviewMovieViewModel())
    }
}

View模型协议和实现:

protocol MovieViewModelable: ObservableObject {
    var movies: [Movie] { get }
    func fetchMovieRatings()
    // Define vars or funcs for anything else your view accesses in your view model
}

class MovieViewModel: MovieViewModelable {

    @Published var movies = [Movie]()

    init() {
       loadNewMovies()
    }

    private func loadNewMovies() {
        // do the network call
    }

    func fetchMovieRatings() {
        // do the network call
    }
}

class PreviewMovieViewModel: MovieViewModelable {
    @Published var movies = [fakeMovie1, fakeMovie2]
    
    func fetchMovieRankings() {} // do nothing while in a Preview
}

这样,MovieListView的外部接口就完全相同了,但是对于预览,您可以使用内部视图定义并覆盖视图模型类型。

mwyxok5s

mwyxok5s3#

对于上面的答案,如果您希望保持您的运输代码库干净,我发现扩展预处理器标志中捕获的类以添加一个方便的init是有效的。

#if DEBUG
extension MovieViewModel{
   convenience init(forPreview: Bool = true) {
      self.init()
      //Hard code your mock data for the preview here
      self.movies = [Movie(...)]
   }
}
#endif

然后也使用预处理器标志修改SwiftUI结构体:

struct MovieListView: View {

   #if DEBUG
   let viewModel: MovieViewModel

   init(viewModel: MovieViewModel = MovieViewModel()){
      self.viewModel = viewModel
   }
   #else
    @StateObject var viewModel = MovieViewModel()
   #endif

    var body: some View {
       List{
        ForEach(viewModel.movies) { movie in
                MovieRow(movie: movie)
                    .listRowInsets(EdgeInsets())
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        MovieListView(viewModel: MovieViewModel(forPreview: true)
    }
}
mzmfm0qo

mzmfm0qo4#

因此,尽管@克雷默的解决方案有效,但我遇到了一个挑战,即当我在设备上调试应用程序时,它将加载预览数据,而不是我希望使用的其他“开发”数据。
因此,我通过创建一个名为“Preview”的新构建配置,然后将所有与“preview”相关的数据 Package 到该构建配置中,对解决方案进行了一些扩展。
这让我可以选择在Xcode预览中预览虚拟数据,同时仍然允许我使用设备/模拟器上的开发数据构建和调试开发版本。
所以我的解现在看起来像这样。

class MovieViewModel: ObservableObject {
   init() {
      #if PREVIEW
         //Hard code your mock data for the preview here
         self.movies = [Movie(...)]
      #else
        // normal init stuff here
      #endif
   }
}
struct MovieListView: View {

   #if PREVIEW
   let viewModel: MovieViewModel

   init(viewModel: MovieViewModel = MovieViewModel()){
      self.viewModel = viewModel
   }
   #else
    @StateObject var viewModel = MovieViewModel()
   #endif

    var body: some View {
       List{
        ForEach(viewModel.movies) { movie in
                MovieRow(movie: movie)
                    .listRowInsets(EdgeInsets())
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        MovieListView(viewModel: MovieViewModel())
    }
}

这可能不是最好的解决办法,但给了我管理与开发/调试数据分开的预览虚拟数据的灵活性,并且到目前为止已经证明对我的用例效果很好。:)

ioekq8ef

ioekq8ef5#

我一直在与此斗争,以及想出了以下简单的解决方案。

//View
struct MyView: View {
    @StateObject private var viewModel = ViewModel()
    
    init(forPreview: Bool = false) {
        guard forPreview else { return }
        let viewModel = ViewModel()
        viewModel.title = "Preview" // Call internal func to load sample data
        _viewModel = StateObject(wrappedValue: viewModel)
    }
    
    var body: some View {
        Text(viewModel.title)
    }
}

//View Model
extension MyView {
    @MainActor class ViewModel: ObservableObject {
        @Published var title: String = "Standard"
    }
}

//Previews
struct MyView_Previews: PreviewProvider {
    static var previews: some View {
        MyView(forPreview: true)
    }
}

@StateObject的初始化是Apple批准的。

相关问题