How to use SWCollaborationView in SwiftUI? - ios

SWCollaborationView was introduced as a standard UI element to manage real-time collaboration between users in iOS16+. This is explained in this Apple article. Similarly to UICloudSharingController, it's way to view participants to a share and manage sharing.
Given that UICloudSharingController is broken in iOS16+ (see this), how can I use SWCollaborationView in SwiftUI?
My failed attempts so far:
The the WWDC22 talk introducing SWCollaborationView, the speaker embedded SWCollaborationView in UIBarButtonItem(customView:). I was not able to embed UIBarButtonItem(customView:) into my SwiftUI lifecycle app, because UIBarButtonItem does not conform to UIView and therefore cannot be introduced using UIViewRepresentable.
I also tried wrapping SWCollaborationView in UIViewRepresentable and introducing it directly. The result was identical to this post. When I wrapped it in ToolbarItem, the icon appeared but no action happened on tap. When I wrapped it in ToolbarItem and Button that would open an identical collaboration view as a popover, the icon appeared (screenshot1) and on tap opened a popover where the same icon would appear (screenshot2. Only a second tap would correctly open the desired popover inherent to SWCollaborationView (screenshot3). The code for this is below.
import SwiftUI
import CloudKit
import SharedWithYou
struct DetailView: View {
var photo: Photo // some object saved in CloudKit, in this example a photo
#State var showPopover = false
#Binding var activeCover: ActiveCover? // for dismissal (not relevant for SWCollaborationView)
var body: some View {
NavigationView {
VStack {
Text("Some content of detail view")
}
.toolbar {
if let existingShare = PersistenceController.shared.existingShare(photo: photo) { // get the existing CKShare for this object (in this case we use NSPersistentCloudKitContainer.fetchShares, but that's not really relevant)
ToolbarItem(placement: .automatic) {
Button(action: {
showPopover.toggle()
}){
CollaborationView(existingShare: existingShare) // <- icon appears, but without the number of participants
}
.popover(isPresented: $showPopover) {
CollaborationView(existingShare: existingShare) // <- icon appears AGAIN, with the number of participants. Instead, we want a popover inherent to SWCollabroationView!
}
}
}
ToolbarItem(placement: .automatic) {
Button("Dismiss") { activeCover = nil }
}
}
}
}
}
struct CollaborationView: UIViewRepresentable {
let existingShare: CKShare
func makeUIView(context: Context) -> SWCollaborationView {
let itemProvider = NSItemProvider()
itemProvider.registerCKShare(existingShare,
container: PersistenceController.shared.cloudKitContainer,
allowedSharingOptions: .standard)
let collaborationView = SWCollaborationView(itemProvider: itemProvider)
collaborationView.activeParticipantCount = existingShare.participants.count
return collaborationView
}
func updateUIView(_ uiView: SWCollaborationView, context: Context) {
}
}

In SwiftUi it's called ShareLink

I submitted a TSI about this; Apple confirmed that SWCollaborationView is not compatible with SwiftUI at the moment and we should submit feedback.

Related

How to open specific View in SwiftUI app using AppIntents

I'm very new to Intents in Swift. Using the Dive Into App Intents video from WWDC 22 and the Booky example app, I've gotten my app to show up in the Shortcuts app and show an initial shortcut which opens the app to the main view. Here is the AppIntents code:
import AppIntents
enum NavigationType: String, AppEnum, CaseDisplayRepresentable {
case folders
case cards
case favorites
// This will be displayed as the title of the menu shown when picking from the options
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Navigation")
static var caseDisplayRepresentations: [Self:DisplayRepresentation] = [
.folders: DisplayRepresentation(title: "Folders"),
.cards: DisplayRepresentation(title: "Card Gallery"),
.favorites: DisplayRepresentation(title: "Favorites")
]
}
struct OpenCards: AppIntent {
// Title of the action in the Shortcuts app
static var title: LocalizedStringResource = "Open Card Gallery"
// Description of the action in the Shortcuts app
static var description: IntentDescription = IntentDescription("This action will open the Card gallery in the Hello There app.", categoryName: "Navigation")
// This opens the host app when the action is run
static var openAppWhenRun = true
#Parameter(title: "Navigation")
var navigation: NavigationType
#MainActor // <-- include if the code needs to be run on the main thread
func perform() async throws -> some IntentResult {
ViewModel.shared.navigateToGallery()
return .result()
}
static var parameterSummary: some ParameterSummary {
Summary("Open \(\.$navigation)")
}
}
And here is the ViewModel:
import SwiftUI
class ViewModel: ObservableObject {
static let shared = ViewModel()
#Published var path: any View = FavoritesView()
// Clears the navigation stack and returns home
func navigateToGallery() {
path = FavoritesView()
}
}
Right now, the Shortcut lets you select one of the enums (Folders, Cards, and Favorites), but always launches to the root of the app. Essentially no different then just telling Siri to open my app. My app uses a TabView in its ContentView with TabItems for the related Views:
.tabItem {
Text("Folders")
Image(systemName: "folder.fill")
}
NavigationView {
GalleryView()
}
.tabItem {
Text("Cards")
Image(systemName: "rectangle.portrait.on.rectangle.portrait.angled.fill")
}
NavigationView {
FavoritesView()
}
.tabItem {
Text("Favs")
Image(systemName: "star.square.on.square.fill")
}
NavigationView {
SettingsView()
}
.tabItem {
Text("Settings")
Image(systemName: "gear")
}
How can I configure the AppIntents above to include something like "Open Favorites View" and have it launch into that TabItem view? I think the ViewModel needs tweaking... I've tried to configure it to open the FavoritesView() by default, but I'm lost on the proper path forward.
Thanks!
[EDIT -- updated with current code]
You're on the right track, you just need some way to do programmatic navigation.
With TabView, you can do that by passing a selection argument, a binding that you can then update to select a tab. An enum of all your tabs works nicely here. Here's an example view:
struct SelectableTabView: View {
enum Tabs {
case first, second
}
#State var selected = Tabs.first
var body: some View {
// selected will track the current tab:
TabView(selection: $selected) {
Text("First tab content")
.tabItem {
Image(systemName: "1.circle.fill")
}
// The tag is how TabView knows which tab is which:
.tag(Tabs.first)
VStack {
Text("Second tab content")
Button("Select first tab") {
// You can change selected to navigate to a different tab:
selected = .first
}
}
.tabItem {
Image(systemName: "2.circle.fill")
}
.tag(Tabs.second)
}
}
}
So in your code, ViewModel.path could be an enum representing the available tabs, and you could pass a binding to path ($viewModel.path) to your TabView. Then you could simply set path = .favorites to navigate.

Deleting the last item in a list from the detail view in SwiftUI does not pop the detail view

I am working on an app and encountered a strange behavior of List and NavigationLink when removing the last item in the list from the detail view. I am using iOS 13 and Xcode 11 and I made a simplified version that reproduces the behavior:
import SwiftUI
struct ListView: View {
#State private var content = [Int](0..<10) {
didSet {
print(content)
}
}
var body: some View {
NavigationView {
List(content, id: \.self) { element in
NavigationLink(
destination: DetailView(
remove: {
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) { // Asynchronous network request
DispatchQueue.main.async {
self.content.removeAll { $0 == element } // When request was successfull remove element from list
}
}
}
)
) {
Text("Element #\(element)")
}
}
}
}
}
struct DetailView: View {
let remove: () -> Void
init(remove: #escaping () -> Void) {
self.remove = remove
}
var body: some View {
VStack {
Text("Hello world!")
Button(
action: {
self.remove()
}
) {
Image(systemName: "trash.circle")
.imageScale(.large)
}
}
}
}
To reconstruct the error select the last item in the list and press the trashcan to delete. As you may notice the view does not disappear as with the other items from the list. But if you press back the list will have correctly removed the last item. This is also shown in this gif. The state change of the list is printed to the console when pressing the trashcan.
I have noticed that the removing of the specific item does not seem to be the problem as it also happens when removing a random item. It does work correctly if I remove the selected item and add new items at the end. So it may be caused by shrinking the size of the array.
I have also found several workarounds. Like modifying the NavigationView with .id(UUID()) but this removes the animations. An other solution is to dismiss the view with PresentationMode and on disappear remove the item from the list but I rather would use a different solution.
To see if it is related to iOS 13 or Xcode 11 I tested it on the newest beta version of iOS 14 and Xcode 12 (currently beta 5). Here the detail view does not get dismissed with any of the selected items.
Has anybody encountered this problem before or at least can explain why this behaves this way?
EDIT: Added mock network request to better illustrate the specific issue.
SwiftUI behaves a bit strange there, I think...
You could tell SwiftUI explicitly to dismiss the DetailView. To do so, you need to modify DetailView a little:
struct DetailView: View {
#Environment(\.presentationMode) var presentationMode
let remove: () -> Void
init(remove: #escaping () -> Void) {
self.remove = remove
}
var body: some View {
VStack {
Text("Hello world!")
Button(
action: {
self.remove()
self.presentationMode.wrappedValue.dismiss()
}
) {
Image(systemName: "trash.circle")
.imageScale(.large)
}
}
}
}
(See also: iOS SwiftUI: pop or dismiss view programmatically)
For me, this seems to work, even with the last row.

SwiftUI NavigationLink push in onAppear immediately pops the view when using #ObservableObject

I want to programmatically be able to navigate to a link within a List of NavigationLinks when the view appears (building deep linking from push notification). I have a string -> Bool dictionary which is bound to a custom Binding<Bool> inside my view. When the view appears, I set the bool property, navigation happens, however, it immediately pops back. I followed the answer in SwiftUI NavigationLink immediately navigates back and made sure that each item in the List has a unique identifier, but the issue still persists.
Two questions:
Is my binding logic here correct?
How come the view pops back immediately?
import SwiftUI
class ContentViewModel: ObservableObject {
#Published var isLinkActive:[String: Bool] = [:]
}
struct ContentViewTwo: View {
#ObservedObject var contentViewModel = ContentViewModel()
#State var data = ["1", "2", "3"]
#State var shouldPushPage3: Bool = true
var page3: some View {
Text("Page 3")
.onAppear() {
print("Page 3 Appeared!")
}
}
func binding(chatId: String) -> Binding<Bool> {
return .init(get: { () -> Bool in
return self.contentViewModel.isLinkActive[chatId, default: false]
}) { (value) in
self.contentViewModel.isLinkActive[chatId] = value
}
}
var body: some View {
return
List(data, id: \.self) { data in
NavigationLink(destination: self.page3, isActive: self.binding(chatId: data)) {
Text("Page 3 Link with Data: \(data)")
}.onAppear() {
print("link appeared")
}
}.onAppear() {
print ("ContentViewTwo Appeared")
if (self.shouldPushPage3) {
self.shouldPushPage3 = false
self.contentViewModel.isLinkActive["3"] = true
print("Activating link to page 3")
}
}
}
}
struct ContentView: View {
var body: some View {
return NavigationView() {
VStack {
Text("Page 1")
NavigationLink(destination: ContentViewTwo()) {
Text("Page 2 Link")
}
}
}
}
}
The error is due to the lifecycle of the ViewModel, and is a limitation with SwiftUI NavigationLink itself at the moment, will have to wait to see if Apple updates the pending issues in the next release.
Update for SwiftUI 2.0:
Change:
#ObservedObject var contentViewModel = ContentViewModel()
to:
#StateObject var contentViewModel = ContentViewModel()
#StateObject means that changes in the state of the view model do not trigger a redraw of the whole body.
You also need to store the shouldPushPage3 variable outside the View as the view will get recreated every time you pop back to the root View.
enum DeepLinking {
static var shouldPushPage3 = true
}
And reference it as follows:
if (DeepLinking.shouldPushPage3) {
DeepLinking.shouldPushPage3 = false
self.contentViewModel.isLinkActive["3"] = true
print("Activating link to page 3")
}
The bug got fixed with the latest SwiftUI release. But to use this code at the moment, you will need to use the beta version of Xcode and iOS 14 - it will be live in a month or so with the next GM Xcode release.
I was coming up against this problem, with a standard (not using 'isActive') NavigationLink - for me the problem turned out to be the use of the view modifiers: .onAppear{code} and .onDisappear{code} in the destination view. I think it was causing a re-draw loop or something which caused the view to pop back to my list view (after approx 1 second).
I solved it by moving the modifiers onto a part of the destination view that's not affected by the code in those modifiers.

NavigationLink freezes when trying to revisit previously clicked NavigationLink in SwiftUI

I am designing an app that includes the function of retrieving JSON data and displaying a list of retrieved items in a FileBrowser type view. In this view, a user should be able to click on a folder to dive deeper into the file tree or click on a file to view some metadata about said file.
I've observed that while this is working, when I click on a file or folder then go back and click on it again, the NavigationLink is not triggered and I am stuck on the view until I click into a different NavigationLink.
Here is a gif demonstrating this problem.
As seen here, when I click on BlahBlah I am activating the NavigationLink and taken to BlahBlah, then when I navigate back and try to renavigate to BlahBlah, it becomes grey, registering that I clicked on it... but then never transports me there. Clicking on TestFile fixes this and allows me to navigate back to BlahBlah.
The list items are made with the following structs
private struct FileCell{
var FileName: String
var FileType: String
var FileID: String = ""
var isContainer: Bool
}
private struct constructedCell: View{
var FileType: String
var FileName: String
var FileID: String
var body: some View {
return
HStack{
VStack(alignment: .center){
Image(systemName: getImage(FileType: FileType)).font(.title).frame(width: 50)
}
Divider()
VStack(alignment: .leading){
Text(FileName).font(.headline)
.multilineTextAlignment(.leading)
Text(FileID)
.font(.caption)
.multilineTextAlignment(.leading)
}
}
}
}
and called into view with navigationLinks as follows
List(cellArray, id: \.FileID) { cell in
if (cell.isContainer) {
NavigationLink(destination: FileView(path: "/\(cell.FileID)", displaysLogin: self.$displaysLogin).navigationBarTitle(cell.FileName)){
constructedCell(FileType: cell.FileType, FileName: cell.FileName, FileID: cell.FileID)
}
} else {
NavigationLink(destination: DetailView(FileID: cell.FileID).navigationBarTitle(cell.FileName)){
constructedCell(FileType: cell.FileType, FileName: cell.FileName, FileID: cell.FileID)
}
}
}
My NavigationView is initialized in the view above (the app has a tab view) this as follows
TabView(selection: $selection){
NavigationView{
FileView(displaysLogin: self.$displaysLogin)
.navigationBarTitle("Home", displayMode: .inline)
.background(NavigationConfigurator { nc in
nc.navigationBar.barTintColor = UIColor.white
nc.navigationBar.titleTextAttributes = [.foregroundColor : UIColor.black]
})
}
.font(.title)
.tabItem {
VStack {
Image(systemName: "folder.fill")
Text("Files")
}
}
.tag(0)
}
The NavigationConfigurator is a struct I use for handling the color of the navigationBar. It is set up like so
struct NavigationConfigurator: UIViewControllerRepresentable {
var configure: (UINavigationController) -> Void = { _ in }
func makeUIViewController(context: UIViewControllerRepresentableContext<NavigationConfigurator>) -> UIViewController {
UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<NavigationConfigurator>) {
if let nc = uiViewController.navigationController {
self.configure(nc)
}
}
}
I do not think my NavigationConfigurator is causing this? This bug also happens in other navigationLinks in the app, but it was easiest to demonstrate it here in the FileBrowser view.
This might be a bug in SwiftUI? If it is does anyone know a way to work around this? If it isn't, what am I doing wrong?
Had the same issue - try this. I would call this a hack to be removed when the bug in swiftUI is corrected.
struct ListView: View {
#State private var destID = 0
...
var body: some View {
...
NavigationLink(destination: FileView(path: "/\(cell.FileID)", displaysLogin: self.$displaysLogin)
.navigationBarTitle(cell.FileName)
.onDisappear() { self.destID = self.destID + 1 }
){
constructedCell(FileType: cell.FileType, FileName: cell.FileName, FileID: cell.FileID)
}.id(destID)
Essentially it seems that in some circumstances (iOS 13.3 - Simulator?) the NavigationLink is not reset when the destination view is removed from the navigation stack. As a work around we need to regenerate the Navigation Link. This is what changing the id does. This corrected my issue.
However if you have NavigationLinks that are chained, that is a link that leads to another list of links, then this solution will create side effects; the stack returns to the origin at the second attempt to show the last view.

Navigate SwiftUI page from WKWebView operation

I am a newbie in IOS Programming and also SwiftUI. Coming from Java/Kotlin Android. I want to learn SwiftUI.
I have a WKWebView. I want to change SwiftUI page according to url changes in WKWebView. I have done a lot of work as you can see below. But I am struggled in navigation.
struct ContentView: View, Listener {
func onFetched() {
NavigationLink(destination: MainView()) {/* HERE this is not working.
App never goes to MainView.swift page.*/
Text("Show Detail View")
}
}
var body: some View {
NavigationView {
VStack {
WebView(authListener: self)
}.navigationBarTitle(Text("PEAKUP Velocity"))
}
}
}
struct WebView: UIViewRepresentable {
var listener: Listener
#ObservedObject var observe = observable()
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
observe.observation = uiView.observe(\WKWebView.url, options: .new) { view, change in
if let url = view.url {
self.observe.loggedIn = true // We loaded the page
self.listener.onFetched()
uiView.isHidden = true
}
}
uiView.load("https://google.com")
}
}
protocol Listener {
func onFetched()
}
Update: I tried this in onFetched():
NavigationLink(destination: MainView()) { Text("") }''''
And I tried this piece of code:
NavigationView {
NavigationLink(destination: SecondView()){
Text("Navigation Link")
}
}
Update 2: I tried this code also in onFetched:
self.presentation(Model(MainView(), onDismiss: nil))
Update 3: I tried this:
self.sheet(isPresented: $sayHello) {
MainView()
}
** Disclaimer I haven't used WKWebviews in the context of SwiftUI, but I assume same would be true. **
I would recommend looking into WKNavigationDelegate. Unless your class is of type WKWebView you need to ensure to assign a delegate to handle any navigation events.
Perhaps you'll find the below article helpful: https://www.hackingwithswift.com/articles/112/the-ultimate-guide-to-wkwebview

Resources