SwiftUI中ObservedObject与DataStore数据传递及URL监听问题
Hey there! Let's break down and fix the two issues you're facing with your SwiftUI setup. I'll walk you through the root causes and step-by-step solutions with updated code.
Issue 1: getTrailers Parameter Assignment & Broken Data Passing
What's Going Wrong:
- You call
getTrailers(url: url ?? "")inDataStore's initializer, buturlisnilat that point—so you're sending an empty string, which won't fetch any trailer data. - When you tap a movie in
ContentView, you setstore.urlbut never triggergetTrailersto run with the new URL. DetailViewcreates a newDataStoreinstance (@ObservedObject var trailer = DataStore()), which is completely separate from the one inContentView. This means it can't see any updates to the originalstore.urlortrailerdata.
Fix Steps:
- Add Combine listeners to
DataStoreto auto-rungetTrailerswheneverurlchanges. - Pass the original
DataStoreinstance fromContentViewtoDetailViewinstead of creating a new one.
Issue 2: Failed url Observation & Data Transfer to DetailView
What's Going Wrong:
DetailView's independentDataStoreinstance means it's listening to the wrongurlproperty (not the one updated inContentView).- You never assign
selectedMovieinContentViewwhen tapping a movie, soDetailViewcan't access the selected movie's data.
Fix Steps:
- Update
ContentViewto setselectedMovieon tap, then pass it toDetailView. - Modify
DetailViewto use the sharedDataStorefromContentViewand display the selected movie's details.
Updated Code Implementations
1. Fixed DataStore (with Combine for Auto-Updates)
import Combine class DataStore: ObservableObject { @Published var movie: MovieRoot? @Published var trailer: TrailerRoot? @Published var url: String? private var cancellables = Set<AnyCancellable>() init() { getMovies() // Auto-run getTrailers whenever url changes $url .compactMap { $0 } // Filter out nil values .sink { [weak self] newURL in self?.getTrailers(url: newURL) } .store(in: &cancellables) } func getMovies() { JSONService().getMovies( url: "https://api.themoviedb.org/3/movie/upcoming?api_key=594b8eb4999a812345136ee3ed1ebdb&language=tr-TR&page=1", model: MovieRoot.self ) { result in self.movie = result } } func getTrailers(url: String) { JSONService().getMovies( url: url, model: TrailerRoot.self ) { result in self.trailer = result } } }
2. Fixed JSONService (Correct Model Type Handling)
Your original getMovies method was passing a model instance instead of a type, which would cause decoding errors. Here's the fix:
class JSONService { func getMovies<T: Decodable>(url: String, model: T.Type, completion: @escaping (T) -> ()) { guard let url = URL(string: url) else { return } URLSession.shared.dataTask(with: url) { data, _, error in guard let data = data, error == nil else { print(error ?? "Unknown error fetching data") return } do { let decodedData = try JSONDecoder().decode(T.self, from: data) DispatchQueue.main.async { completion(decodedData) } } catch let decodingError { print("Decoding error: \(decodingError)") } }.resume() } }
3. Fixed ContentView (Proper Selection & Data Passing)
struct ContentView: View { @State private var show = false @State private var selectedMovie: Movie? @ObservedObject var store = DataStore() private let imageURL = "https://image.tmdb.org/t/p/w185_and_h278_bestv2" var body: some View { ZStack { ScrollView(.horizontal, showsIndicators: false) { HStack { ForEach(store.movie?.results ?? []) { item in GeometryReader { geometry in VStack { WebImage(url: URL(string: imageURL + item.poster_path)) .resizable() .retryOnAppear(true) .renderingMode(.original) .cornerRadius(30) .frame(width: 185, height: 278) .shadow(color: .black.opacity(0.4), radius: 10, x: 0, y: 10) .animation(.spring(response: 0.5, dampingFraction: 0.6)) Text(item.title) .font(.system(size: 20, weight: .bold)) CircleView( firstColor: #colorLiteral(red: 0.2392156869, green: 0.6745098233, blue: 0.9686274529, alpha: 1), secondColor: #colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1), width: 50, height: 50, percent: item.vote_average ) } .rotation3DEffect( Angle(degrees: Double(geometry.frame(in: .global).minX - 30) / -20), axis: (x: 0, y: 10.0, z: 0) ) .frame(width: UIScreen.main.bounds.width) .onTapGesture { show.toggle() selectedMovie = item // Set the trailer URL for the selected movie store.url = "https://api.themoviedb.org/3/movie/\(item.id)/videos?api_key=594b8eb4999a8b44ad5136ee3ed1ebdb&language=en-US" } .offset(y: show ? .zero : -UIScreen.main.bounds.height) .animation(.easeIn) } .frame(width: UIScreen.main.bounds.width) } } } .frame(maxHeight: .infinity) // Show DetailView only when a movie is selected if let selectedMovie = selectedMovie { DetailView(show: $show, selectedMovie: selectedMovie, store: store) } } } }
4. Fixed DetailView (Shared DataStore & Dynamic Trailer)
struct DetailView: View { @Binding var show: Bool let selectedMovie: Movie @ObservedObject var store: DataStore private let baseYouTubeURL = "https://www.youtube.com/watch?v=" var body: some View { ScrollView(.vertical, showsIndicators: false) { VStack { Image(systemName: "multiply.circle") .imageScale(.large) .padding() .onTapGesture { show.toggle() } // Display trailer if available, else show placeholder if let trailerKey = store.trailer?.results.first?.key { WebView(request: URLRequest(url: URL(string: baseYouTubeURL + trailerKey)!)) .frame(height: UIScreen.main.bounds.height * 0.3) .background(Color(UIColor.secondarySystemBackground)) .cornerRadius(20) .shadow(color: .black.opacity(0.3), radius: 10, x: 0, y: 10) } else { Text("No trailer available") .frame(height: UIScreen.main.bounds.height * 0.3) .background(Color(UIColor.secondarySystemBackground)) .cornerRadius(20) .shadow(color: .black.opacity(0.3), radius: 10, x: 0, y: 10) } HStack { Text(selectedMovie.title) .font(.system(size: 20, weight: .semibold)) Spacer() CircleView( firstColor: #colorLiteral(red: 0.2196078449, green: 0.007843137719, blue: 0.8549019694, alpha: 1), secondColor: #colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1), width: 30, height: 30, percent: selectedMovie.vote_average ) } .padding(.horizontal) .background(Color(UIColor.secondarySystemBackground)) .cornerRadius(10) .shadow(color: .black.opacity(0.3), radius: 5, x: 0, y: 5) .padding() Text(selectedMovie.overview) .padding() .background(Color(UIColor.secondarySystemBackground)) .cornerRadius(20) .padding(.horizontal) .shadow(color: .black.opacity(0.3), radius: 10, x: 0, y: 10) } } .animation(.easeIn) .offset(y: show ? .zero : UIScreen.main.bounds.height) } }
Key Takeaways
- Share DataStore Instances: Never create new
DataStoreobjects across views if you want them to sync data. Pass the existing instance or use@EnvironmentObjectfor even easier cross-view access. - Combine for Auto-Updates: Using
@Publishedproperties with Combine'ssinklets you automatically trigger actions (like API calls) when data changes. - Proper Model Handling: Ensure your JSON decoder receives the correct type (not an instance) to avoid decoding failures.
内容的提问来源于stack exchange,提问作者Ufuk Köşker
相关产品推荐
相关产品推荐

