Reputation: 106
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:
Widget in Home screen:
Upvotes: 2
Views: 881
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
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