Perform a deeplink from SwiftUI widget on tap - ios

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.

Related

Is there no way to detect when a SwiftUI view is dismissed?

I have an app that is built using a NavigationSplitView with a menu on the left and a map on the right. The left view controls the state of the map depending on what view is currently shown in the menu. Previously I saved my own routing state model for the navigation when NavigationLinks where activated using tags and selection. This made it possible to know the exact state of the apps routing at all times. With the new NavigationStack, we have to use NavigationPath which can not be monitored since the internal values are private.
Another option we had previously for knowing when a view was dismissed was to create a StateObject for the view when the view was created, then it will be deallocated as the view is dismissed. However that won't work in NavigationStack since the new .navigationDestination is called multiple times like any type of view rendering, making the StateObject allocate and deallocate just as many times.
And yes, I know about .onAppear and .onDisappear. However, these events are irrelevant in this situation since they can be called multiple times during the views lifecycle e.g. when another view is presented on top of the current view etc.
Is it possible to detect when a view truly disappears (is dismissed) in SwiftUI?
This isn't an answer to how to detect when a screen disappears, but rather a solution to the first part of your problem.
With a NavigationStack, you don't have to use a NavigationPath object as the path.
The initialiser is:
init(path: Binding<Data>, #ViewBuilder root: () -> Root) where Data : MutableCollection, Data : RandomAccessCollection, Data : RangeReplaceableCollection, Data.Element : Hashable
so path can be a Binding of any array who's elements are Hashable. e.g.
struct ContentView: View {
enum Routing: Hashable {
case screen1, screen2(String)
}
#State private var path: [Routing] = []
var body: some View {
NavigationStack(path: $path) {
List {
NavigationLink("Show screen 1", value: Routing.screen1)
NavigationLink("Show screen 2", value: Routing.screen2("Fred"))
}
.navigationDestination(for: Routing.self) { screen in
switch screen {
case .screen1:
Text("This is screen 1")
case let .screen2(name):
Text("This is screen 2 - name: \(name)")
}
}
}
.onChange(of: path) { newValue in
path.forEach { screen in
print(screen)
}
}
}
}
As your path is not an opaque object you can use that to determine your app's current state.

SwiftUI: modal Sheet hides ProgressView

I'm trying to have a system-wide progress bar in my SwiftUI application, so I defined this view (be aware: I'm targeting iOS 13+):
import SwiftUI
struct LoadingView<Content>: View where Content: View {
#Binding var isShowing: Bool
var content: () -> Content
var body: some View {
GeometryReader { _ in
ZStack {
self.content()
if self.isShowing {
VStack {
ActivityIndicator()
Text("Loading...")
}
}
}
}
}
}
struct ActivityIndicator: UIViewRepresentable {
typealias UIView = UIActivityIndicatorView
fileprivate var configuration = { (_: UIView) in }
func makeUIView(context: UIViewRepresentableContext<Self>) -> UIView { UIView() }
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<Self>) {
uiView.startAnimating()
configuration(uiView)
}
}
and is used in ContenView.swift like this
import SwiftUI
struct ContentView: View {
#EnvironmentObject var myViewModel: MyViewModel
var body: some View {
let isLoading = Binding<Bool>(
get: { self.myViewModel.isLoading },
set: { _ in }
)
LoadingView(isShowing: isLoading) {
NavigationView {
Home()
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
where MyViewModel is a pretty standard ViewModel with a #Published var for isLoading, and Home() is the main entry point for the app.
Whenever some action that trigger isLoading in MyViewModel is done, the progress bar is shown (and hidden) correctly.
But, if I present a .sheet either by one of the view inside the NavigationView, or using the ContentView itself, no matter what it hides the progress bar.
Even if I use the builtin 14+ ProgressView the problem persists.
.Zindex() does not help either.
Any way to have that view always on top when showed, no matter what .sheet, .alert or any overlay view is available on SwiftUI is present on the screen?
Thanks in advance!
As already written in the comments, a modal view will be shown on top of any other view. A modal view is meant to establish a Computer-Human communication, or dialog (thus modal views frequently will be named "Dialog").
The observation, that a sheet (modal view) covers the loading indicator is expected behaviour.
But, IMO the issue described in the question and refined in the comments, can be solved nicely without breaking the behaviour of the modal views:
When you want to show data, that is not yet complete or even completely absent, you may show a "blank" screen, and in additions to this, let the view model generate a view state that says, that the view should show an "Input Sheet".
So initially, the user sees an input form over a blank screen.
Once the user made the input and submits the form (which will be handled in the View Model) the input sheet disappears (controlled by the View State generated by the View Model), and reveals the "blank" view underneath it.
So, the View Model could now present another sheet, or it realises that the input is complete.
Once the input is complete, the view model loads data and since this may take a while, it reflects this in the View State accordingly, for example using a "loading" state or flag. The view renders this accordingly, which is a loading indicator above the "blank" view.
When the view model receives data, it clears the loading state and sets the view state accordingly, passing through the data as well.
The view now renders the data view.
If the loading task failed, the view model composes a view state where the content is "absent" and with an error info.
Again the view renders this, possibly showing an alert with the message above a "blank" view, since there is still no data.
Ensure, the user can dismiss the error alert and the view model handles it by removing the "modal error" state, but the content is still "absent".
Now, the user is starring at a blank view. You may embed an error message here, or even add a "Retry" button. In any case, ensure the user can navigate away from that screen.
And so on. ;)

SwiftUI on app load - redirect to web URL

In my iOS app, I have a requirement to load a web URL on app load or app icon is tapped by the user.
application landing page has a UI but it will only appear after user press back button from the browser.
How to redirect to any URL as soon as app is open? I am using swift UI.
You can use NavigationView with pre-selected NavigationLink: this will open as soon as the view appears.
Display your main app only after NavigationLink is closed with if.
struct ContentView: View {
var body: some View {
MainApp()
.onAppear {
UIApplication.shared.open(URL(string: "https://stackoverflow.com/")!)
}
}
}
struct MainApp: View {
var body: some View {
Text("hello")
}
}

SwiftUI DocumentGroup and switching to background

I have made an app based on the new SwiftUI multi platform target for a "Document based app".
However, I face weird issues. As long as an app is in the foreground, it works just fine. If it is moved to the background by task switching, and then again to the foreground, mutations are being saved to the document, but the SwiftUI Views don't receive mutations. So whenever you press a button in the UI that mutates the document you see nothing happening while the mutation is there once you reload the document from disk.
So i am thinking, I use ObservedObjects, they probably get kicked out of memory once I move to the background. could this be the cause of my bug?
But then I added a print line to the App struct.
import SwiftUI
#main
struct MyApp: App {
fileprivate func myLogging(_ file: FileDocumentConfiguration<MyDocument>) -> some View {
print("""
IT IS CALLED
""")
return MainView().environmentObject(BindingWrapper(file.$document))
}
var body: some Scene {
DocumentGroup(newDocument: MyDocument()) { (file) in
return myLogging(file)
}.commands { AppCommands() }
}
}
and guess what... this print always executes just before a mutation is being rendered. Which makes sense. because file.$document is a binding, and if you do a mutating action, the binding will warn Apple that the file is dirty, but it will also invalidate the entire hierarchy. This logging will still print once the bug has occurred!
So on the line MainView().environmentObject(BindingWrapper(file.$document)) I assume everything is created from scratch. BindingWrapper is a custom class I made to convert a binding in an observable object. And this is one of the objects I worried about, that they might be freed. but if they are created newly.... they should be always there, right?
And by the way, this object is owned by the environment. So it should not be freed.
So, now I am stuck. is Apple doing some clever caching on bindings / ObservedObjects which will inject old objects into my view hierarchy even though I think everything is created newly?
Try moving any wiring/instantiation to the first view of the document group. If that view houses StateObjects you expect to share the lifetime of the document window, they will not be rebuilt.
In the example below, a WindowStore is housed as an #StateObject as described. A RootStore housed in App creates the WindowStore, which includes vending services and registering it in a managed array of windows. Either could enable your logging service. (For me, that array helps WindowGroups operate on a specific document when #FocusedValue would fail (i.e., the top-most document is no longer the key window).)
#main
struct ReferenceFileDoc: App {
#StateObject var root: RootStore
var body: some Scene {
DocumentGroup { ProjectDocument() } editor: { doc in
DocumentGroupRoot(
window: root.makeWindowStore(doc.document),
factory: SwiftUIFactory(root, doc.document)
)
.environmentObject(doc.document)
.environment(\.documentURL, doc.fileURL)
.injectStores(from: root)
}.commands { Menus(root: root) }
.... other scenes ...
struct DocumentGroupRoot: View {
#EnvironmentObject var doc: ProjectDocument
#Environment(\.undoManager) var undoManager
#Environment(\.documentURL) var url
#StateObject var window: WindowStore
#StateObject var factory: UIFactory
var body: some View {
passUndoManagerToDocument()
factory.reference(window)
return DocumentWindow(vm: factory.makeThisVM()) // Actual visible window
.focusedValue(\.keyWindow, window)
.focusedValue(\.keyDocument, doc)
.onAppear { /// Tasks }
.reportHostingNSWindow { [weak window] in
window?.setWindow($0)
}
.onChange(of: url) { [weak window] in window?.setFileURL($0) }
.environmentObject(/// sub-state stores from WindowStore)
.environmentObject(window)
.environmentObject(factory)
}
}

widgetURL is override inside Foreach?

I am displaying 3 rows in iOS 14 medium size widget like below:
row 1
-------
row 2
-------
row 3
-------
my view structure is here:
VStack {
ForEach(records, id: \.id) { item in
ZStack {
// some views
}
.widgetURL(URL(string: "wig://\(item.id)"))
}
Divider()
}
It seems the widget URL for first and second items are override by the the third item, all deep link will open third item's content.
what is the proper way to add deep link for views generated from ForEach?
Here is interface contract (pay attention at marked)
/// Sets the URL to open in the containing app when the user clicks the widget.
/// - Parameter url: The URL to open in the containing app.
/// - Returns: A view that opens the specified URL when the user clicks
/// the widget.
///
>> /// Widgets support one `widgetURL` modifier in their view hierarchy.
>> /// If multiple views have `widgetURL` modifiers, the behavior is
/// undefined.
public func widgetURL(_ url: URL?) -> some View
Instead you have to use Link, like
ForEach(records, id: \.id) { item in
Link(destination: URL(string: "wig://\(item.id)")!) {
ZStack {
// some views
}
}
}

Resources