Paul
Paul

Reputation: 4430

Swiftui widget ios 14 problems with image downloaded from the internet

ui

I have to put a background image from the internet in a widget, but it gives me the following problem.

You can tell me where I'm wrong?

Note.swift(model)

import Foundation

struct Note: Identifiable, Codable {
    let title: String
    let message: String

    var id = UUID()
}

SmallWidget.swift


import SwiftUI

struct SmallWidget: View {
    var entry: Note
    @Environment(\.colorScheme) var colorScheme
    
    var body: some View {
        
        VStack(alignment: .center){
            Text(entry.title)
                .font(.title)
                .bold()
                .minimumScaleFactor(0.5)
                .foregroundColor(.white)
                .shadow(
                    color: Color.black,
                    radius: 1.0,
                    x: CGFloat(4),
                    y: CGFloat(4))
            Text(entry.message)
                .foregroundColor(Color.gray)
                .shadow(
                    color: Color.black,
                    radius: 1.0,
                    x: CGFloat(4),
                    y: CGFloat(4))

        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .edgesIgnoringSafeArea(.all)
        .background(NetworkImage(url: URL(string: "https://a.wattpad.com/useravatar/climaxmite.256.718018.jpg")))
    }
}

struct SmallWidget_Previews: PreviewProvider {
    static var previews: some View {
        let note = Note(title: "Title", message: "Mex")
        
        Group {
            SmallWidget(entry: note)
            SmallWidget(entry: note)
                .preferredColorScheme(.dark)
        }
    }
}

note.swift

import WidgetKit
import SwiftUI

struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(note: Note(title: "Title", message: "placeholder"))
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(note: Note(title: "Title", message: "getSnapshot"))
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []

        // Generate a timeline consisting of five entries an hour apart, starting from the current date.
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(note: Note(title: "Title", message: "getTimeline"))
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

struct SimpleEntry: TimelineEntry {
    public let note: Note
    public let date: Date = Date()
}

struct noteEntryView : View {
    /*var entry: Provider.Entry

    var body: some View {
        Text(entry.date, style: .time)
    }*/
    
    var entry: Provider.Entry
    @Environment(\.widgetFamily) private var widgetFamily
    
    var body: some View {
        switch widgetFamily {
        case .systemSmall:
            SmallWidget(entry: entry.note)
        default:
            SmallWidget(entry: entry.note)
        }
    }
}

@main
struct note: Widget {
    let kind: String = "note"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            noteEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}

struct note_Previews: PreviewProvider {
    static var previews: some View {
        noteEntryView(entry: SimpleEntry(note: Note(title: "Title", message: "Mex")))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

NetworkImage.swift



import Foundation
import Combine
import SwiftUI

extension NetworkImage {
    class ViewModel: ObservableObject {
        @Published var imageData: Data?
        @Published var isLoading = false

        private var cancellables = Set<AnyCancellable>()

        func loadImage(from url: URL?) {
            isLoading = true
            guard let url = url else {
                isLoading = false
                return
            }
            URLSession.shared.dataTaskPublisher(for: url)
                .map { $0.data }
                .replaceError(with: nil)
                .receive(on: DispatchQueue.main)
                .sink { [weak self] in
                    self?.imageData = $0
                    self?.isLoading = false
                }
                .store(in: &cancellables)
        }
    }
}

// Download image from URL
struct NetworkImage: View {
    @StateObject private var viewModel = ViewModel()

    let url: URL?

    var body: some View {
        Group {
            if let data = viewModel.imageData, let uiImage = UIImage(data: data) {
                Image(uiImage: uiImage)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            } else if viewModel.isLoading {
                ProgressView()
            } else {
                Image(systemName: "photo")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .redacted(reason: /*@START_MENU_TOKEN@*/.placeholder/*@END_MENU_TOKEN@*/)
            }
        }
        .onAppear {
            viewModel.loadImage(from: url)
        }
    }
}

Upvotes: 1

Views: 2521

Answers (1)

Marcus Adams
Marcus Adams

Reputation: 53830

Unfortunately, no async image loader is going to work in a widget since widgets are static. They're declarative, but not dynamic. They're not allowed to change over time.

You can download the image in getSnapshot() and getTimeline() before calling the completion handler, then pass the image along with data in the entry.

Here's some pseudo code:

struct SimpleEntry: TimelineEntry {
    public let note: Note
    public let date: Date = Date()
}

func placeholder(in context: Context) -> SimpleEntry {
    // Some placeholder note + image
    SimpleEntry(note: note, date: Date())
}

func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
  // fetch note and image
  ...

  note.image = image

  let entry = SimpleEntry(note: note, date: Date())
  completion(entry)
}

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
  var entries: [SimpleEntry] = []
  
  // fetch note and image
  ...

  note.image = image

  let entry = SimpleEntry(note: note, date: Date())
  entries.append(entry)

  let timeline = ...
  completion(timeline)
}

struct noteEntryView : View {
    var entry: Provider.Entry

    @Environment(\.widgetFamily) private var widgetFamily
    
    @ViewBuilder
    var body: some View {
        switch widgetFamily {
        case .systemSmall:
            SmallWidget(note: entry.note)
        default:
            SmallWidget(note: entry.note)
        }
    }
}

struct SmallWidget: View {
    let note: Note

    var body: some View {
      Image(uiImage: note.image)
      ...
    }
}

Upvotes: 1

Related Questions