widgetURL is override inside Foreach? - ios

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

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.

Perform a deeplink from SwiftUI widget on tap

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.

How can I set and use the argument "selection" in List in SwiftUI

I have learned about SwiftUI, and am having difficulties to understand List in SwiftUI.
The List definition is below.
#available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct List<SelectionValue, Content> : View where SelectionValue : Hashable, Content : View {
/// Creates a List that supports multiple selection.
///
/// - Parameter selection: A binding to a set that identifies the selected
/// rows.
///
/// - See Also: `View.selectionValue` which gives an identifier to the rows.
///
/// - Note: On iOS and tvOS, you must explicitly put the `List` into Edit
/// Mode for the selection to apply.
#available(watchOS, unavailable)
public init(selection: Binding<Set<SelectionValue>>?, #ViewBuilder content: () -> Content)
/// Creates a List that supports optional single selection.
///
/// - Parameter selection: A binding to the optionally selected row.
///
/// - See Also: `View.selectionValue` which gives an identifier to the rows.
///
/// - Note: On iOS and tvOS, you must explicitly put the `List` into Edit
/// Mode for the selection to apply.
#available(watchOS, unavailable)
public init(selection: Binding<SelectionValue?>?, #ViewBuilder content: () -> Content)
:
:
}
Then my question is this, how can I have List that supports multiple/single selection?
I would know how to set argument of Binding<Set<SelectionValue>>? and Binding<Set<SelectionValue>>?.
I have read How does one enable selections in SwiftUI's List already, and I've got this code. This code does support multiple selection.
var demoData = ["Phil Swanson", "Karen Gibbons", "Grant Kilman", "Wanda Green"]
struct ContentView: View {
#State var selectKeeper = Set<String>()
var body: some View {
NavigationView {
List(demoData, id: \.self, selection: $selectKeeper){ name in
Text(name)
}
.navigationBarItems(trailing: EditButton())
.navigationBarTitle(Text("Selection Demo \(selectKeeper.count)"))
}
}
}
But still can't understand how I can set the argument "selection" and set the type as well. How can I change to single selection List? What is Set<String>()...?
Does anyone explain easily to understand?
I would have easy example...
Thank you so much, Sensei! Thank you for reading my question!!
How can I change to single selection List?
#State var selectKeeper: String? = nil // << default, no selection
What is Set()...?
A container for selected items, in your case strings from demoData
how I can set the argument "selection" and set the type as well
One variant is in .onAppear as below
List(demoData, id: \.self, selection: $selectKeeper){ name in
Text(name)
}
.onAppear {
self.selectKeeper = [demoData[0]]
}
Type of selected is detected by type of state variable, it if Set that it is multi-selection, if it is optional, then single selection.

What does 'identity' mean in SwifUI and how do we change the 'identity' of something

I'm new to SwiftUI and I'm having problems with presenting Alerts back-to-back.
The description of the .alert(item:content:) modifier has this written in it's definition:
/// Presents an alert.
///
/// - Parameters:
/// - item: A `Binding` to an optional source of truth for the `Alert`.
/// When representing a non-nil item, the system uses `content` to
/// create an alert representation of the item.
///
/// If the identity changes, the system will dismiss a
/// currently-presented alert and replace it by a new alert.
///
/// - content: A closure returning the `Alert` to present.
public func alert<Item>(item: Binding<Item?>, content: (Item) -> Alert) -> some View where Item : Identifiable
I'm particularly interested in the If the identity changes, the system will dismiss a currently-presented alert and replace it by a new alert part. Since I want Alerts to be presented back-to-back, if I'm somehow able to change the 'identity', I'll be able to achieve the functionality that I want - which is having the system dismiss the currently-presented alert and replacing the old Alert with a new Alert (back-to-back).
If someone can explain to me what 'identity' is and how I can change the 'identity' of something I'll be extremely grateful.
(Or if you know a better way to present alerts back-to-back that'll also be very very helpful.)
Thanks in advance!
Find below demo of alert-by-item usage. And some investigation results about changing identity as documented.
As you find experimenting with example (by tap on row) alert activated by user interaction works fine, but changing identity programmatically, as documented, seems not stable yet, however alert is really updated.
Tested with Xcode 11.4 / iOS 13.4
struct SomeItem: Identifiable { // identifiable item
var id: Int // identity
}
struct DemoAlertOnItem: View {
#State private var selectedItem: SomeItem? = nil
var body: some View {
VStack {
ScrollView (.vertical, showsIndicators: false) {
ForEach (0..<5) { i in
Text("Item \(i)").padding()
.onTapGesture {
self.selectedItem = SomeItem(id: i)
// below simulate change identity while alert is shown
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
self.selectedItem = nil // works !!
// self.selectedItem?.id = 100 // crash !!
// self.selectedItem = SomeItem(id: 100) // crash !!
}
}
}
}
}
.alert(item: self.$selectedItem) { item in
Alert(title: Text("Alert"), message: Text("For item \(item.id)"), dismissButton: .default(Text("OK")))
}
}
}
Notice how this method requires a Binding<Item?>, and that Item should be Identifiable. For the Binding<Item?> parameter, you're supposed to pass in a "source of truth" that controls what the alert shown looks like, or whether the alert shows at all. When this source of truth changes (i.e. becomes something else), the view will update the alert.
But here's the problem, how does SwiftUI know what does "change" mean in the context of your model? Let's say Item is a Person class that you wrote. Person has a name and age. It is your job to tell SwiftUI, that "a Person becomes a totally different person when its name changes". (Of course, you could have some other definition of what is meant by "a person changes" here. This definition is just an example.)
struct Person : Identifiable {
var id: String {
name
}
let name: String
let age: Int
}
This is why Item must be Identifiable. Item.id is thus the "identity".
Note that Identifiable is different from Equatable, in that Identifiable asks the question "what makes this person a different person?" whereas Equatable asks "what result would you want == to give?". See this for another example.
how do we change the 'identity' of something?
Just change the binding you pass in (e.g. setting the #State that the binding is based on) in such a way that its id changes.

SwiftUI: NavigationLink pops immediately if used within ForEach

I'm using a NavigationLink inside of a ForEach in a List to build a basic list of buttons each leading to a separate detail screen.
When I tap on any of the list cells, it transitions to the detail view of that cell but then immediately pops back to the main menu screen.
Not using the ForEach helps to avoid this behavior, but not desired.
Here is the relevant code:
struct MainMenuView: View {
...
private let menuItems: [MainMenuItem] = [
MainMenuItem(type: .type1),
MainMenuItem(type: .type2),
MainMenuItem(type: .typeN),
]
var body: some View {
List {
ForEach(menuItems) { item in
NavigationLink(destination: self.destination(item.destination)) {
MainMenuCell(menuItem: item)
}
}
}
}
// Constructs destination views for the navigation link
private func destination(_ destination: ScreenDestination) -> AnyView {
switch destination {
case .type1:
return factory.makeType1Screen()
case .type2:
return factory.makeType2Screen()
case .typeN:
return factory.makeTypeNScreen()
}
}
If you have a #State, #Binding or #ObservedObject in MainMenuView, the body itself is regenerated (menuItems get computed again) which causes the NavigationLink to invalidate (actually the id change does that). So you must not modify the menuItems arrays id-s from the detail view.
If they are generated every time consider setting a constant id or store in a non modifying part, like in a viewmodel.
Maybe I found the reason of this bug...
if you use iOS 15 (not found iOS 14),
and you write the code NavigationLink to go to same View in different locations in your projects, then this bug appear.
So I simply made another View that has different destination View name but the same contents... then it works..
you can try....
sorry for my poor English...

Resources