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

WidgetKit SwiftUI主屏幕小组件内容显示异常问题求助

WidgetKit小组件主屏幕显示异常(彩色方块替代预期内容)

问题现象

使用WidgetKit开发SwiftUI小组件时,SwiftUI画布预览完全正常(显示模拟数据的预期布局),但主屏幕上的小组件内容显示为彩色方块,而非预期的文本和UI元素。


相关代码

1. Provider与Widget配置代码

import WidgetKit
import SwiftUI
import Intents

//MARK: - 小组件数据Provider
struct Provider: TimelineProvider {
    typealias Entry = WidgetEntry
    
    // 占位视图数据
    func placeholder(in context: Context) -> WidgetEntry {
        WidgetEntry(date: Date(),
                    topThreeMovies: [dummyMovie, dummyMovie, dummyMovie])
    }

    // 添加小组件时的预览数据
    func getSnapshot(in context: Context, completion: @escaping (WidgetEntry) -> ()) {
        let entry = WidgetEntry(date: Date(),
                                topThreeMovies: [dummyMovie, dummyMovie, dummyMovie])
        completion(entry)
    }
    
    // 刷新时间线逻辑
    func getTimeline(in context: Context, completion: @escaping (Timeline<WidgetEntry>) -> ()) {
        let movieListViewModel = MovieListViewModel()

        movieListViewModel.fetchTopThreeMovies { topThreeMovies in
            var entries: [WidgetEntry] = []
            let currentDate = Date()
            
            let entry = WidgetEntry(date: Date(),
                                    topThreeMovies:topThreeMovies )
            
            let refreshTime = Calendar.current.date(byAdding: .minute, value: 1, to: currentDate)
            entries.append(entry)
            
            let timeline = Timeline(entries: entries, policy: .after(refreshTime!))
            completion(timeline)
        }
    }
}

// MARK: - 小组件入口视图
struct TopMoviesWidgetEntryView: View {
    var entry: Provider.Entry
    @Environment(\.widgetFamily) var family
    
    @ViewBuilder
    var body: some View {
        switch family {
        case .systemMedium:
            MovieWidgetView(topThreeMovies: [dummyMovie, dummyMovie, dummyMovie])
        default:
            fatalError("Unsupported widget family")
        }
    }
}

// MARK: - 小组件配置
struct TopMoviesWidget: 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: - 预览
struct TopMoviesWidget_Previews: PreviewProvider {
    static var previews: some View {
        TopMoviesWidgetEntryView(entry: WidgetEntry(date: Date(),
                                                    topThreeMovies: [dummyMovie, dummyMovie, dummyMovie]))
            .previewContext(WidgetPreviewContext(family: .systemMedium))
    }
}

2. 小组件视图代码

import SwiftUI
import WidgetKit

/// 电影榜单小组件视图
struct MovieWidgetView: View {
    var topThreeMovies: [Movie]
    
    var body: some View {
        HStack(alignment: .top) {
            // 日期与列表按钮区域
            VStack(alignment: .leading) {
                Text("\(Date().getFormattedWeekDay())")
                    .foregroundColor(.orange)
                    .bold()
                Text("\(Date().getFormattedDate())")
                    .bold()
                
                Spacer()
                
                Button(action: {
                    // 跳转至电影列表页逻辑
                }) {
                    Label("", systemImage: "list.bullet.circle.fill").body
                        .foregroundColor(.orange)
                        .font(.system(size: 30))
                }
            }
            .padding(EdgeInsets(top: 20, leading: 0, bottom: 0, trailing: 10))
            
            Spacer()
            
            // 前三电影列表
            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)
                        
                        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()
    }
}

// 视图预览
struct MovieWidgetView_Previews: PreviewProvider {
    static var previews: some View {
        MovieWidgetView(topThreeMovies: [dummyMovie, dummyMovie, dummyMovie])
            .previewContext(WidgetPreviewContext(family: .systemMedium))
    }
}

3. ViewModel代码

import SwiftUI
import WidgetKit

final class MovieListViewModel: ObservableObject {
    @Published var movies: [Movie] = []
    @Published var topThreeMovies: [Movie] = []
    
    @Published var isShowingDetail = false
    @Published var selectedMovie: Movie?
    
    // 加载电影列表
    func fetchMovies() {
        isLoading = true
        let loadedMovies = PlistManager.shared.loadMoviesFromPlist(resourceName: "TopMovies")
        movies = loadedMovies.shuffled()
        WidgetCenter.shared.reloadAllTimelines()
        print("Loaded \(movies.count) movies from plist.")
    }
    
    // 加载小组件用的前三电影
    func fetchTopThreeMovies(completion: @escaping ([Movie]) -> Void) {
        isLoading = true
        for i in 0..<3 {
            topThreeMovies[i] = movies[i]
        }
        print("topThreeMovies array has \(movies.count) movies from plist.")
        
        WidgetCenter.shared.reloadAllTimelines()
        completion(topThreeMovies)
    }
    
    // 获取电影排名索引
    func getIndex(for movie: Movie) -> Int? {
        return movies.firstIndex { $0.id == movie.id }
    }
}

效果对比

预览效果(正常)

含模拟数据的小组件快照

主屏幕异常效果

主屏幕上的小组件


问题根源与修复方案

1. 视图未使用Entry传递的真实数据

在TopMoviesWidgetEntryView中,你硬编码使用模拟数据,而非利用entry中的真实数据:

// 错误写法
MovieWidgetView(topThreeMovies: [dummyMovie, dummyMovie, dummyMovie])

// 正确写法
MovieWidgetView(topThreeMovies: entry.topThreeMovies)

预览正常是因为预览本身使用模拟数据,但主屏幕会使用Provider返回的真实Entry数据,未正确传递会触发后续数据异常。

2. ViewModel数据加载逻辑错误

fetchTopThreeMovies存在数组越界风险:

  • topThreeMovies初始为空数组,直接通过下标赋值会崩溃
  • 未调用数据加载逻辑,movies数组始终为空

修复代码:

func fetchTopThreeMovies(completion: @escaping ([Movie]) -> Void) {
    let loadedMovies = PlistManager.shared.loadMoviesFromPlist(resourceName: "TopMovies")
    movies = loadedMovies.shuffled()
    topThreeMovies = Array(movies.prefix(3))
    completion(topThreeMovies)
}

3. 移除致命错误调用

TopMoviesWidgetEntryView中的fatalError会导致Widget崩溃,替换为安全的默认视图:

// 错误写法
default:
    fatalError("Unsupported widget family")

// 正确写法
default:
    Text("不支持的小组件尺寸")
        .padding()

4. 可选值安全处理

getTimeline中强制解包refreshTime会导致崩溃,添加安全判断:

if let refreshTime = refreshTime {
    let timeline = Timeline(entries: entries, policy: .after(refreshTime))
    completion(timeline)
} else {
    let timeline = Timeline(entries: entries, policy: .atEnd)
    completion(timeline)
}

内容的提问来源于stack exchange,提问作者Tanjila Nur

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 01:37:03