iOS WidgetKit: remote images fails to load - swiftui

I'm observing a bizarre thing: in the new widgets far too often remote images are not being displayed even though the image has been successfully loaded and placed in cache.
For image downloading I've tried:
SDWebImageSwiftUI
Kingfisher
SwURL
All of them indicate that image loading succeed, but the actual widget is not showing it.
struct TestWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
WebImage(url: URL(string: "https://miro.medium.com/max/3840/0*TLqp5Uwavd-U_xrs.jpg"))
.onSuccess()
.resizable()
}
}
On the second run of the debugger - with image loading from cache - I get the image displayed, but never(?) on initial run.
It feels like that in onSuccess I need to trigger UI-invalidation? But how?
(Since it happens to literally every image-lib I try - I don't think that it's something off in the libs)
Environment:
iOS 14 Beta 3 (both device and simulators)
Xcode 12 Beta 3
During the debug run, memory use is around 15mb

Yes, as mentioned by Konstantin, it is not supported to load images asynchronously. There are 2 options to load network image in widgets
Either fetch all the images in TimelineProvider and inject them to the views directly.
OR
Use Data(contentsOf: url) to fetch the image. This way it still fetches them synchronously but the code is cleaner. Sample code -
struct NetworkImage: View {
private let url: URL?
var body: some View {
Group {
if let url = url, let imageData = try? Data(contentsOf: url),
let uiImage = UIImage(data: imageData) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
}
else {
Image("placeholder-image")
}
}
}
}
This view can simply be use like this -
NetworkImage(url: url)

Got it: it's simply not supported and you aim to load images inside TimelineProvider:
https://developer.apple.com/forums/thread/652581

Related

SwiftUI: Keep VideoPlayer playing while device is locked

title says it all as far as I'm aware. I have the following view:
struct VideoView: View{
#State var FilePath = ""
#State private var player = AVPlayer()
var body: some View{
VideoPlayer(player: player)
.onAppear{
player = AVPlayer(url: URL(string: FilePath)!)
player.play()
}.onDisappear{
player.pause()
}
}
}
I'm calling this in my ContentView, and it works fine. But when I close the app, or when the device is locked, the video stops playback. Is there a way that I can change this so that it will keep playing?
It took a while to figure it out, but I eventually came up with the following solution.
make sure that you enable Audio, AirPlay and Picture in Picture under Signing and Capabilities
When you define your AVPlayer, add the following code:
player = AVPlayer(url: URL(string: FilePath)!)
do{
try AVAudioSession.sharedInstance().setCategory(.playback)
try AVAudioSession.sharedInstance().setActive(true)
}catch{
print(error.localizedDescription)
}
player.audiovisualBackgroundPlaybackPolicy = .continuesIfPossible
You're done, your video will now play when your device is locked or you are in another app. Note that playback controls do not show on the simulator, however it works fine on the device.

SwiftUI Image not switching to dark mode when created using UIImage

I have a simple SwiftUI view where I create a Image view using a UIImage. The issue is that the Image created with the UIImage is not switching to the dark or light mode when the color scheme changes. It works correctly only if the entire view is reloaded. It works as expected if I use create the image directly with the image name.
struct ContentView: View {
var body: some View {
HStack {
VStack {
Image("my-image")
Text("Image")
}
VStack {
Image(uiImage: UIImage(named: "my-image")!)
Text("Image+uiImage")
}
}
}
}
The image itself is in a xcasset catalog
result is this (note the right image not changing when the appearance changes but only after restarting the app)
This happens when the SwiftUI Image is initialized using an UIImage (seems to be a static initialization with no further observation of the original UIImage).
Workaround: The view can be informed to redraw manually, when using the colorScheme environment variable. Even when it seems to be unused in your later code.
This is pretty useful, if you need to consume UIImages from a shared UIKit/SwiftUI codebase.
In your example:
struct ContentView: View {
// This is required for the automatic re-creation of the view
// (and thus updating the Image with the appropriate appearance
// for the UIImage)
#Environment(\.colorScheme) private var colorScheme // <- view get's notified
var body: some View {
HStack {
VStack {
Image("my-image")
Text("Image")
}
VStack {
Image(uiImage: UIImage(named: "my-image")!)
Text("Image+uiImage")
}
}
}
}
Be aware that this might lead to unneeded redrawing, so try to use SwiftUI Images first, as #Jan said.
The workaround here is to use Image without using the UIImage constructor

Can't open Deeplinks with different tails Widgets SwiftUI [duplicate]

I have a simple widget (medium-sized) with two texts, and what I want is to be able to perform a deep link to lead the user to a specific section of my app, but I can't seem to find a way to do so.
The view I have written (which is very simple):
HStack {
Text("FIRST ITEM")
Spacer()
Text("SECOND ITEM")
}
I have already tried to replace
Text("SECOND ITEM")
with
Link("SECOND ITEM destination: URL(string: myDeeplinkUrl)!)
but it doesn't work either.
In the Widget view you need to create a Link and set its destination url:
struct SimpleWidgetEntryView: View {
var entry: SimpleProvider.Entry
var body: some View {
Link(destination: URL(string: "widget://link1")!) {
Text("Link 1")
}
}
}
Note that Link works in medium and large Widgets only. If you use a small Widget you need to use:
.widgetURL(URL(string: "widget://link0")!)
In your App view receive the url using onOpenURL:
#main
struct WidgetTestApp: App {
var body: some Scene {
WindowGroup {
Text("Test")
.onOpenURL { url in
print("Received deep link: \(url)")
}
}
}
}
It is also possible to receive deep links in the SceneDelegate by overriding:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>)
You can find more explanation on how to use this function in this thread:
Detect app launch from WidgetKit widget extension
Here is a GitHub repository with different Widget examples including the DeepLink Widget.
Also, you can do it using AppDelegate (if you not using SceneDelegate):
.widgetURL(URL(string: "urlsceheme://foobarmessage"))
// OR
Link(destination: URL(string: "urlsceheme://foobarmessage")!) {
Text("Foo")
}
Set this code within AppDelegate
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
let message = url.host?.removingPercentEncoding // foobarmessage
return true
}
See docs on: Respond to User Interactions
When users interact with your widget, the system launches your app to handle the request. When the system activates your app, navigate to the details that correspond to the widget’s content. Your widget can specify a URL to inform the app what content to display. To configure custom URLs in your widget:
For all widgets, add the widgetURL(_:) view modifier to a view in your widget’s view hierarchy. If the widget’s view hierarchy includes more than one widgetURL modifier, the behavior is undefined.
For widgets that use WidgetFamily.systemMedium or WidgetFamily.systemLarge, add one or more Link controls to your widget’s view hierarchy. You can use both widgetURL and Link controls. If the interaction targets a Link control, the system uses the URL in that control. For interactions anywhere else in the widget, the system uses the URL specified in the widgetURL view modifier.
For example, a widget that displays details of a single character in a game can use widgetURL to open the app to that character’s detail.
#ViewBuilder
var body: some View {
ZStack {
AvatarView(entry.character)
.widgetURL(entry.character.url)
.foregroundColor(.white)
}
.background(Color.gameBackground)
}
If the widget displays a list of characters, each item in the list can be in a Link control. Each Link control specifies the URL for the specific character it displays.
When the widget receives an interaction, the system activates the containing app and passes the URL to onOpenURL(perform:), application(_:open:options:), or application(_:open:), depending on the life cycle your app uses.
If the widget doesn’t use widgetURL or Link controls, the system activates the containing app and passes an NSUserActivity to onContinueUserActivity(_:perform:), application(_:continue:restorationHandler:), or application(_:continue:restorationHandler:). The user activity’s userInfo dictionary contains details about the widget the user interacted with. Use the keys in WidgetCenter.UserInfoKey to access these values from Swift code. To access the userInfo values from Objective-C, use the keys WGWidgetUserInfoKeyKind and WGWidgetUserInfoKeyFamily instead.

WKWebView in SwiftUI not loading HTML string on macOS

first question on StackOverflow (and relatively new to native app dev targeting macOS).
I'm currently trying to build a simple SwiftUI view, that leverages WKWebView's loadHTMLString function, to display hardcoded HTML string on the screen.
AFAIK webkit does not support SwiftUI as of the moment, so I need to embed AppKit UI in my SwiftUI app using NSViewRepresentable. This what I got so far following the docs and XCode's autocomplete:
import SwiftUI
import WebKit
struct HTMLView: NSViewRepresentable {
typealias NSViewType = WKWebView
let html = "<h1>Hello wordl</h1>"
func makeNSView(context: Context) -> WKWebView {
let webview = WKWebView()
return webview
}
func updateNSView(_ nsView: WKWebView, context: Context) {
nsView.loadHTMLString(html, baseURL: nil)
}
}
struct HTMLView_Previews: PreviewProvider {
static var previews: some View {
HTMLView()
}
}
Should be noted that preview canvas does not load the HTML (displays empty window).
I then replace the default Text() view in ContentView.Swift with HTMLView(), and run my application.
The application compiles, but the WebView fails to load the HTML (I get an empty window). I get the following errors in console:
WebPageProxy::processDidTerminate: (pid 0), reason 3
WebPageProxy::dispatchProcessDidTerminate: reason = 3
WebPageProxy::processDidTerminate: (pid 0), reason 3
WebPageProxy::dispatchProcessDidTerminate: reason = 3
WebPageProxy::tryReloadAfterProcessTermination: process crashed and the client did not handle it, not reloading the page because we reached the maximum number of attempts
Any help with the above would be highly appreciated!
On macOS, although it doesn't seem like this should be necessary, you need to set "Outgoing Connections (Client)" to true in your "Signing and Capabilities" on your target in order for WKWebView to load, even though you're loading from a string and not from an external page.
As soon as I changed this, your example worked fine.

Snapshotting a view that has not been rendered results in an empty snapshot when view presented modally - Swift 3

I have a Table View Controller as a tab. In it, there's a button that takes you to a Facebook profile:
func didTapFacebook() {
let url = URL(string: "http://www.facebook.com/" + myFacebookId)
if UIApplication.shared.canOpenURL(url!) {
UIApplication.shared.open(url!, options: [:], completionHandler: nil)
}
}
Works fine every time you press the button.
From another tab, there's a button that presents modally a navigation controller with Table View Controller as its root:
func segueToTable(_ sender: UIViewController, tvc: MyTableViewController, completion: #escaping ((_ done: Bool) -> Void)) {
let nc = MyNavigationController(rootViewController: tvc)
sender.present(nc, animated: true, completion: {
completion(true)
})
}
…
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let tvc = storyboard.instantiateViewController(withIdentifier: "TVC") as! MyTableViewController
segueToTable(self, tvc: tvc, completion: { done in
print(“segue complete”)
})
Now once you tap the same button to go to Facebook profile (or Twitter button, basically anything that causes the app to go to background), this warning occurs (there is no keyboard displayed on the screen at this time):
Cannot snapshot view (>) with afterScreenUpdates:NO, because the view is not in a window. Use afterScreenUpdates:YES.
Returning to the app and pressing the button again causes this warning to appear (and every attempt after that):
Snapshotting a view that has not been rendered results in an empty snapshot. Ensure your view has been rendered at least once before snapshotting or snapshot after screen updates.
I realize this is related to snapshotting a view that automatically occurs prior to entering the background. It appears for some reason it doesn't like doing that within a view that was presented modally. I've tried a variety of things based on other posts but cannot get the warnings to go away.
Any help is appreciated.