You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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:

  1. You call getTrailers(url: url ?? "") in DataStore's initializer, but url is nil at that point—so you're sending an empty string, which won't fetch any trailer data.
  2. When you tap a movie in ContentView, you set store.url but never trigger getTrailers to run with the new URL.
  3. DetailView creates a new DataStore instance (@ObservedObject var trailer = DataStore()), which is completely separate from the one in ContentView. This means it can't see any updates to the original store.url or trailer data.

Fix Steps:

  • Add Combine listeners to DataStore to auto-run getTrailers whenever url changes.
  • Pass the original DataStore instance from ContentView to DetailView instead of creating a new one.

Issue 2: Failed url Observation & Data Transfer to DetailView

What's Going Wrong:

  1. DetailView's independent DataStore instance means it's listening to the wrong url property (not the one updated in ContentView).
  2. You never assign selectedMovie in ContentView when tapping a movie, so DetailView can't access the selected movie's data.

Fix Steps:

  • Update ContentView to set selectedMovie on tap, then pass it to DetailView.
  • Modify DetailView to use the shared DataStore from ContentView and 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 DataStore objects across views if you want them to sync data. Pass the existing instance or use @EnvironmentObject for even easier cross-view access.
  • Combine for Auto-Updates: Using @Published properties with Combine's sink lets 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.07 08:12:37