1

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)
        }
    }
}

Paul
  • 3,644
  • 9
  • 47
  • 113
  • It’s probably your progressview. You can’t have anything animating in a widget. – Andrew Oct 20 '20 at 16:09
  • @Andrew: I don't think you take this project from which I was inspired, use the same thing: https://github.com/Arjun-dureja/SubWidget/blob/main/SubscriberWidget/View/NetworkImage.swift – Paul Oct 20 '20 at 16:17
  • In that case it seems to work, but NetworkImage is giving problems this way. – Paul Oct 20 '20 at 16:20
  • The no entry ⛔️ sign usually means that you have added something that is disallowed in a widget. The most obvious thing is the progressview as that will animate and widgets are static. Other offenders are things like scrollviews. – Andrew Oct 20 '20 at 16:33
  • 1
    The no sign is for the `ProgressView` but the whole concept will not work in Widgets. Views here are static - you can't use a `@StateObject` or nothing similar. The only way to refresh a view is by recreating a timeline. What you can do is to create a network request *outside* the view. See [How to refresh Widget data?](https://stackoverflow.com/q/63976424/8697793). – pawello2222 Oct 20 '20 at 17:31

1 Answers1

1

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)
      ...
    }
}
Marcus Adams
  • 53,009
  • 9
  • 91
  • 143
  • Can you guide me how to download image? I tried with `Data(contentsOf: url)` without success of showing on UIImage – Neo.Mxn0 Jan 08 '21 at 06:47
  • 1
    @Neo.Mxn0, I used `SDWebImagePrefetcher.shared.prefetchURLs()` inside `getTimeline()` then referred to the cache in the SwiftUI using `SDImageCache.shared.imageFromCache()` . – Marcus Adams Jan 08 '21 at 16:12
  • 1
    I've just got success with `Data(contentsOf: url)` :D – Neo.Mxn0 Jan 11 '21 at 09:46