Tanjila Nur
Tanjila Nur

Reputation: 106

WidgetKit SwiftUI Widget not displaying content properly on the home screen

I'm working on a SwiftUI Widget using WidgetKit, and I've encountered an issue where the widget's content is not displaying correctly on the home screen. The widget content appears as colored boxes instead of showing the expected text and elements. However, the preview in SwiftUI Canvas works perfectly fine.

Here's my code for the widget:

import WidgetKit
import SwiftUI
import Intents


//MARK: - Creating provider for providing data for widget

struct Provider: TimelineProvider {
    typealias Entry = WidgetEntry
    
    ///Placeholder to show static data to user
    func placeholder(in context: Context) -> WidgetEntry {
        WidgetEntry(date: Date(),
                    topThreeMovies: [dummyMovie, dummyMovie, dummyMovie])
    }

    ///Snapshot to use a preview when adding widgets
    func getSnapshot(in context: Context, completion: @escaping (WidgetEntry) -> ()) {
        /// Initial snapshot or loading type widget
        let entry = WidgetEntry(date: Date(),
                                topThreeMovies: [dummyMovie, dummyMovie, dummyMovie])
        completion(entry)
    }
    
    /// This function refreshes the widget for events
    func getTimeline(in context: Context, completion: @escaping (Timeline<WidgetEntry>) -> ()) {
        
        let movieListViewModel = MovieListViewModel()

        // Fetch the top three movies using the view model
        movieListViewModel.fetchTopThreeMovies { topThreeMovies in
            
            /// Initializing empty array for entries of WidgetEntry
            var entries: [WidgetEntry] = []
            
            ///Set value for currentDate
            let currentDate = Date()
            
            /// Specifying  data for entry
            let entry = WidgetEntry(date: Date(),
                                    topThreeMovies:topThreeMovies )
            
            /// Refresh widget every 15 minutes
            let refreshTime = Calendar.current.date(byAdding: .minute, value: 1, to: currentDate)
            entries.append(entry)
            
            /// Creating Timeline
            let timeline = Timeline(entries: entries, policy: .after(refreshTime!))
            
            completion(timeline)
        }
    }
}

// MARK: - Top Movies Widget Entry View

struct TopMoviesWidgetEntryView: View {
    
    var entry: Provider.Entry
    
    /// Access the widget family environment variable
    @Environment(\.widgetFamily) var family
    
    @ViewBuilder
    var body: some View {
        
        /// Switch based on the widget family
        switch family {
            
        case .systemMedium:
            /// Display the MovieWidgetView for the medium widget size
            MovieWidgetView(topThreeMovies: [dummyMovie, dummyMovie, dummyMovie])
            
        default:
            /// Handle other widget family types (fatalError is used here for simplicity)
            fatalError("Unsupported widget family")
        }
    }
}


// MARK: - Widget Configuration

struct TopMoviesWidget: Widget {
    
    /// Unique identifier for the widget
    let kind: String = "TopMoviesWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            TopMoviesWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("Top Movies Widget")
        .description("This is a widget of Today's Top Movies.")
        .supportedFamilies([.systemMedium])
    }
}

// MARK: - Create a preview for the widget entry view

struct TopMoviesWidget_Previews: PreviewProvider {
    static var previews: some View {
        TopMoviesWidgetEntryView(entry: WidgetEntry(date: Date(),
                                                    topThreeMovies: [dummyMovie, dummyMovie, dummyMovie]))
            .previewContext(WidgetPreviewContext(family: .systemMedium))
    }
}

Here's my code for the widget View:

import SwiftUI
import WidgetKit


/// Top Movies Widget View
struct MovieWidgetView: View {
    
//    @StateObject var viewModel = MovieListViewModel()
    var topThreeMovies: [Movie]
    
    var body: some View {
        HStack(alignment: .top) {
            
            /// Show Week Day, Date and List Button
            VStack(alignment: .leading) {
                
                ///Week Day
                Text("\(Date().getFormattedWeekDay())")
                    .foregroundColor(.orange)
                    .bold()
                ///Date
                Text("\(Date().getFormattedDate())")
                    .bold()
                
                Spacer()
                
                ///List Button to navigate to Movie List screen
                Button(action: {
                    /// Close the widget view and navigate to MovieListView
                }) {
                    Label("", systemImage: "list.bullet.circle.fill").body
                        .foregroundColor(.orange)
                        .font(.system(size: 30))
                }
            }
            .padding(EdgeInsets(top: 20, leading: 0, bottom: 0, trailing: 10))
            
            Spacer()
            
            ///Top Three movies Listr
            VStack(alignment: .leading) {
                ForEach(0..<3, id: \.self) { i in
                    Text("\(i+1). \(topThreeMovies[i].name)")
                        .lineLimit(1)
                    
                    HStack {
                        Image(systemName: "star.fill")
                            .foregroundColor(.orange)
                        
                        /// Movie Rating & No. of Rates
                        Text("\(String(format: "%.1f", topThreeMovies[i].rating))" )
                        
                        Text("(\(topThreeMovies[i].noOfRates))")
                            .foregroundColor(.secondary)
                    }
                    
                    Divider()
                }
            }
            .font(.subheadline)
            .padding(EdgeInsets(top: 10, leading: 0, bottom: 0, trailing: 0))
        }
        .padding()
        
    }
}

/// Preview to see how the view looks
struct MovieWidgetView_Previews: PreviewProvider {
    static var previews: some View {
        MovieWidgetView(topThreeMovies: [dummyMovie, dummyMovie, dummyMovie]).previewContext(WidgetPreviewContext(family: .systemMedium))
    }
}

Here's my code for the View Model:

import SwiftUI
import WidgetKit

final class MovieListViewModel: ObservableObject {
    
    ///These variable are for movie list view
    @Published var movies: [Movie] = []
    
    /// Top 3 movies for widget
    @Published var topThreeMovies: [Movie] = []
    
    ///These variable are for movie details view
    @Published var isShowingDetail = false
    @Published var selectedMovie: Movie?
    
    ///Fetch movies for Movie List Screen
    func fetchMovies() {
        isLoading = true
        
        /// Call the function to load movies from the plist
        let loadedMovies = PlistManager.shared.loadMoviesFromPlist(resourceName: "TopMovies")
        
        /// Shuffle the loadedMovies array to mimic the data source update
        movies = loadedMovies.shuffled()
        WidgetCenter.shared.reloadAllTimelines()
        
        print("Loaded \(movies.count) movies from plist.")
    }
    
    ///Fetch movies for Movie widget
    func fetchTopThreeMovies(completion: @escaping ([Movie]) -> Void) {
        isLoading = true
        
        ///get top three movies from all movies
        for i in 0..<3 {
            topThreeMovies[i] = movies[i]
        }
        print("topThreeMovies array has \(movies.count) movies from plist.")
        
        WidgetCenter.shared.reloadAllTimelines()
        completion(topThreeMovies)
    }
    
    /// get index values to use as ranks
    func getIndex(for movie: Movie) -> Int? {
        return movies.firstIndex { $0.id == movie.id }
    }

Widget snapshot with dummy data:

enter image description here

Widget in Home screen:

enter image description here

Upvotes: 2

Views: 881

Answers (2)

Thorsten Stark
Thorsten Stark

Reputation: 127

It looks like the views are redacted. This is a known behavior (issue?) with widgets. You can try to display the unredacted view by adding the .unredacted() modifier to your view, like this:

case .systemMedium:
            /// Display the MovieWidgetView for the medium widget size
            MovieWidgetView(topThreeMovies: [dummyMovie, dummyMovie, dummyMovie])
                .unredacted()

Upvotes: 0

matthias_code
matthias_code

Reputation: 1033

This part here might be an issue:

  ForEach(0..<3, id: \.self) { i in
    Text("\(i+1). \(topThreeMovies[i].name)")

Where you go through 3 entries of the array, but are not checking if the array is actually long enough, and contains the required element.

I had the same issue in one of my projects, where an array was accessed out of bounds, and the entire thing stayed in the skeleton-loading view :)

Upvotes: 0

Related Questions