How to use Transferable in an asynchronous way in SwiftUI? - ios

I'd like to drag a file in my application (representing a file on a network drive) to the user's Desktop or Finder window. Consequently the file should only be downloaded once the drag & drop operation ended.
Using AppKit I could achieve this behavior using File Promises. But for SwiftUI, there seems to be no equivalent. The only option is to use NSItemProvider or Transferable (or both in combination) (see this question). But neither NSItemProvider nor Transferable support this asynchronous behavior.
The main problem is that itemProvider closure and also the FileRepresentation closure is called when the dragging session starts, thus also starting the download of the files. This makes the feature unusable e.g. if you drag a large file or many files at once.
struct Item: Identifiable, Transferable {
let id = UUID()
let name: String
let description: String
let url: URL
static var transferRepresentation: some TransferRepresentation {
FileRepresentation(contentType: .fileURL) { (item: DragItem) in
let urlSession = URLSession(configuration: .default)
// This download is already started when the drag & drop session starts
let (tempURL, response) = try! await urlSession.download(from: item.url)
return SentTransferredFile(tempURL)
}
}
}
let items = [
Item(name: "Name 1", description: "Description 1", url: URL(string: "https://...")),
Item(name: "Name 2", description: "Description 2", url: URL(string: "https://...")),
Item(name: "Name 3", description: "Description 3", url: URL(string: "https://..."))
]
struct Test: View {
var body: some View {
Table {
TableColumn("Column 1") { item in
Text(item.name)
}
TableColumn("Column 2") { item in
Text(item.description)
}
} rows: {
ForEach(items) { item in
TableRow(item)
.itemProvider {
let provider = NSItemProvider()
provider.register(item)
return provider
}
}
}
}
}
How can I achieve such an asynchronous behavior in SwiftUI as it was possible in AppKit using NSFilePromiseProvider and NSFilePromiseProviderDelegate?

Related

SwiftUI design pattern - How to make a "2nd-stage" API call from the result of the first one

This is on iOS 15.5 using the latest SwiftUI standards.
I have these two structs in my SwiftUI application:
User.swift
struct User: Codable, Identifiable, Hashable {
let id: String
let name: String
var socialID: String? // it's a var so I can modify it later
func getSocialID() async -> String {
// calls another API to get the socialID using the user's id
// code omitted
// example response:
// {
// id: "aaaa",
// name: "User1",
// social_id: "user_1_social_id",
// }
}
}
Video.swift
struct Video: Codable, Identifiable, Hashable {
let id: String
let title: String
var uploadUser: User
}
My SwiftUI application displays a list of videos, the list of videos are obtained from an API (which I have no control over), the response looks like this:
[
{
id: "AAAA",
title: "My first video. ",
uploaded_user: { id: "aaaa", name: "User1" },
},
{
id: "BBBB",
title: "My second video. ",
uploaded_user: { id: "aaaa", name: "User1" },
},
]
My video's view model looks like this:
VideoViewModel.swift
#MainActor
class VideoViewModel: ObservableObject {
#Published var videoList: [Video]
func getVideos() async {
// code simplified
let (data, _) = try await URLSession.shared.data(for: videoApiRequest)
let decoder = getVideoJSONDecoder()
let responseResult: [Video] = try decoder.decode([Video].self, from: data)
self.videoList = responseResult
}
func getSocialIDForAll() async throws -> [String: String?] {
var socialList: [String: String?] = [:]
try await withThrowingTaskGroup(of: (String, String?).self) { group in
for video in self.videoList {
group.addTask {
return (video.id, try await video.uploadedUser.getSocialId())
}
}
for try await (userId, socialId) in group {
socialList[userId] = socialId
}
}
return socialList
}
}
Now, I wish to fill in the socialID field for the User struct, which I must obtain from another API using each user's ID. the response looks like this for each user:
{
id: "aaaa",
name: "User1",
social_id: "user_1_social_id",
}
Right now the only viable way to get the information seems to be using withThrowingTaskGroup() and call getSocialID() for each user, which I am using right now, then I can return a dictionary that contains all the socialID information for each user, then the dictionary can be used in SwiftUI views.
But, is there a way for me to fill in the socialID field in the User struct without having to use a separate dictionary? It doesn't seem like I can modify the User struct in each Video inside videoList once the JSON decoder initializes the list of videos, due to the fact that VideoViewModel is a MainActor. I would prefer to have everything downloaded in one go, so that when the user enters a subview, there is no loading time.
You are correct that you can't modify the structs once they are initialized, because all of their properties are let variables; however, you can modify the videoList in VideoViewModel, allowing you to dispense with the Dictionary.
#MainActor
class VideoViewModel: ObservableObject {
#Published var videoList: [Video]
func getVideos() async {
// code simplified
let (data, _) = try await URLSession.shared.data(for: videoApiRequest)
let decoder = getVideoJSONDecoder()
let responseResult: [Video] = try decoder.decode([Video].self, from: data)
self.videoList = try await Self.getSocialIDForAll(in: responseResult)
}
private static func updatedWithSocialID(_ user: User) async throws -> User {
return User(id: user.id, name: user.name, socialID: try await user.getSocialID())
}
private static func updatedWithSocialID(_ video: Video) async throws -> Video {
return Video(id: video.id, title: video.title, uploadUser: try await updatedWithSocialID(video.uploadUser))
}
static func getSocialIDForAll(in videoList: [Video]) async throws -> [Video] {
return try await withThrowingTaskGroup(of: Video.self) { group in
videoList.forEach { video in
group.addTask {
return try await self.updatedWithSocialID(video)
}
}
var newVideos: [Video] = []
newVideos.reserveCapacity(videoList.count)
for try await video in group {
newVideos.append(video)
}
return newVideos
}
}
}
Using a view model object is not standard for SwiftUI, it's more of a UIKit design pattern but actually using built-in child view controllers was better. SwiftUI is designed around using value types to prevent the consistency errors typical for objects so if you use objects then you will still get those problems. The View struct is designed to be the primary encapsulation mechanism so you'll have more success using the View struct and its property wrappers.
So to solve your use case, you can use the #State property wrapper, which gives the View struct (which has value semantics) reference type semantics like an object would, use this to hold the data that has a lifetime matching the View on screen. For the download, you can use async/await via the task(id:) modifier. This will run the task when the view appears and cancel and restart it when the id param changes. Using these 2 features together you can do:
#State var socialID
.task(id: videoID) { newVideoID in
socialID = await Social.getSocialID(videoID: newViewID)
}
The parent View should have a task that got the video infos.

Displaying results of database query in list - SwiftUI & Supabase

I'm trying to practice using Supabase with SwiftUI as an alternative to Firebase. I have a table set up in my Supabase database and my code is identifying this and retrieving the data, but I'm having issues displaying it in a list once it's been retrieved.
I can print the data to the console window to show it has in fact been retrieved but the List doesn't seem to display anything on the simulator screen.
My view model contains this code:
let client = SupabaseClient(supabaseUrl: SupabaseURL, supabaseKey: SupabaseKey)
#Published var todos: [Todo] = []
// Retrieve all todo's
func getAllTodos() {
print("Retrieving all todo's from database")
let query = client.database.from("todos")
.select()
query.execute { results in
switch results {
case let .success(response):
let todos = try? response.decoded(to: [Todo].self)
self.todos = todos!
print("Todos: \(todos)")
case let .failure(error):
print(error.localizedDescription)
}
}
}
And my ContentView contains this code:
#State private var model = ContentViewModel()
#State var title = ""
#State var description = ""
var body: some View {
VStack {
TextField("Title", text: $title)
.padding(5)
.border(Color.black)
TextField("Description", text: $description)
.padding(5)
.border(Color.black)
Button("Create Todo") {
model.createTodo(title: title, description: description)
}
.padding(10)
Button("List todo's") {
model.getAllTodos()
}
.padding(10)
List(model.todos) { todo in
Text("\(todo.title): \(todo.description)")
Text("Done: \(String(describing: todo.isDone))")
}
.refreshable {
model.getAllTodos()
}
}
.padding(20)
.onAppear {
model.getAllTodos()
}
}
If I run the simulator, and click 'List todo's' once, the console shows this output:
Retrieving all todo's from database
2022-01-30 17:59:19.077191+0000 Supabase API 2[36461:1087300] [boringssl] boringssl_metrics_log_metric_block_invoke(151) Failed to log metrics
Todos: [Supabase_API_2.Todo(id: "56e81c25-2fbe-45da-ac57-5e0d92fdd9b4", title: "First Todo", description: "basic description", isDone: false), Supabase_API_2.Todo(id: "06b8e7bf-f990-4553-9c92-5a12a38d8e78", title: "Second todo", description: "another basic description", isDone: false)]
Retrieving all todo's from database
Todos: [Supabase_API_2.Todo(id: "56e81c25-2fbe-45da-ac57-5e0d92fdd9b4", title: "First Todo", description: "basic description", isDone: false), Supabase_API_2.Todo(id: "06b8e7bf-f990-4553-9c92-5a12a38d8e78", title: "Second todo", description: "another basic description", isDone: false)]
You can see it retrieves the todo's when the view loads, then again when I click the 'List todo's' button. So I can see it is correctly retrieving the data and storing in the 'todos' var, but for some reason won't display it in the list.
Any help with this would be really appreciated!
Assuming ContentViewModel is an ObservableObject (which it should be), it should be annotated with #StateObject (which is the property wrapper that allows the View to hook up to the objects publisher), not #State.

How do I filter my data displayed in my view?

My XCODE Swift VIEW code below displays a list of all Flavor Groups and Descriptors from my data. What I would like to do is filter the data to display all Flavor Groups and Descriptors except where isSeltzer is false.
I have tried using something like this in my View Model and then using iterating from the filtered array in my view but I can't get this to work:
let flavorsNoSeltzers = flavors.filter({ return $0.isSeltzer != false })
Here is example of my local JSON data:
[
{
"id": "562811",
"flavorGroup": "APRICOT",
"name": "NATURAL AND ARTIFICIAL APRICOT FLAVOR",
"isBeer": true,
"isSeltzer": false,
"isArtificial": true,
"descriptors": ["FRUITY"],
"keywords": ["juicy", "skunky", "peach", "floral", "slight green (sierra nevada pale ale)"]
},
{
"id": "U39252",
"flavorGroup": "BANANA",
"name": "NATURAL BANANA FLAVORING",
"isBeer": true,
"isSeltzer": true,
"isArtificial": false,
"descriptors": [""],
"keywords": ["missing"]
},
{
"id": "681686",
"flavorGroup": "WHITE CHOCOLATE",
"name": "NATURAL WHITE CHOCOLATE FLAVOR WONF",
"isBeer": true,
"isSeltzer": true,
"isArtificial": false,
"descriptors": ["LACTONIC", "COCOA", "CREAMY"],
"keywords": ["nutty", "milk chocolate", "french vanilla", "custard", "cakey"]
}
]
Here is an example of my MODEL:
struct Flavor: Codable, Identifiable {
enum CodingKeys: CodingKey {
case id
case flavorGroup
case name
case isBeer
case isSeltzer
case isArtificial
case descriptors
case keywords
}
let id, flavorGroup, name: String
let isBeer, isSeltzer, isArtificial: Bool
let descriptors, keywords: [String]
}
Here is an example of my VIEW MODEL:
class ReadData: ObservableObject {
#Published var flavors = [Flavor]()
init(){
loadData()
}
func loadData() {
guard let url = Bundle.main.url(forResource: "flavors", withExtension: "json")
else {
print("Json file not found")
return
}
let data = try? Data(contentsOf: url)
let flavors = try? JSONDecoder().decode([Flavor].self, from: data!)
self.flavors = flavors!
}
}
Here is an example of my VIEW:
struct myView: View {
#ObservedObject var flavorData = ReadData()
var body: some View{
List(flavorData.flavors){ flavor in
VStack(alignment: .leading) {
Text(flavor.flavorGroup)
ForEach(flavor.descriptors, id: \.self) { descriptor in
if descriptor.isEmpty {
// do nothing
} else {
Text("- \(descriptor)")
}
}
}
}
}
}
A simplified working example that filters the list of Flavor objects and returns a new list with only Flavor objects where isSeltzer is true.
import Foundation
struct Flavor {
let id: String
let isSeltzer: Bool
}
let flavors = [Flavor(id: "First", isSeltzer: false), Flavor(id: "Second", isSeltzer: true), Flavor(id: "Third", isSeltzer: false)]
let flavorsSeltzersTrue = flavors.filter({ $0.isSeltzer })
print(flavorsSeltzersTrue) // Prints the flavors with isSeltzer == true
If you want the opposite (i.e. where isSeltzer is false), you can simply change $0.isSeltzer in the closure to !$0.isSeltzer. Also, check out the documentation on the filter(_:) method.
If this doesn't fix your issue, then there's something else going on with your code.
EDIT:
In your code, you might want to add this line of code like this:
class ReadData: ObservableObject {
#Published var flavors = [Flavor]()
init(){
loadData()
}
func loadData() {
guard let url = Bundle.main.url(forResource: "flavors", withExtension: "json")
else {
print("Json file not found")
return
}
let data = try? Data(contentsOf: url)
let flavors = try? JSONDecoder().decode([Flavor].self, from: data!)
self.flavors = flavors.filter({ $0.isSeltzer }) // I've added it here.
}
}
Please note that force unwrapping (flavors!) is bad practice and might lead to crashes when flavors is nil.

Wordpress REST API + Swift

I'm a novice developer trying to write a simple app that presents posts from a Wordpress site as a feed. I'm using the Wordpress REST API and consuming that within swift. I'm getting stuck at parsing the JSON and presenting it in swift.
Detail below, but how do I code the dual identifier of 'title' + 'rendered' from the REST API?
So far I've got this in swift:
import SwiftUI
struct Post: Codable, Identifiable {
let id = UUID()
var title.rendered: String
var content.rendered: String
}
class Api {
func getPosts(completion: #escaping ([Post]) -> ()) {
guard let url = URL(string: "https://councillorzamprogno.info/wp-json/wp/v2/posts") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
guard let data = data else { return }
let posts = try! JSONDecoder().decode([Post].self, from: data)
DispatchQueue.main.async {
completion(posts)
}
}
.resume()
}
but the "var title.rendered: String" isn't accepted by Xcode, I get the error "Consecutive declarations on a line must be seperated by ';'. So how should I go about getting the post tile, content etc. when it appears like this in the REST API:
{
id: 1216,
date: "2020-11-18T00:51:37",
date_gmt: "2020-11-17T13:51:37",
guid: {
rendered: "https://councillorzamprogno.info/?p=1216"
},
modified: "2020-11-18T01:31:52",
modified_gmt: "2020-11-17T14:31:52",
slug: "the-nsw-2020-state-redistribution",
status: "publish",
type: "post",
link: "https://councillorzamprogno.info/2020/11/18/the-nsw-2020-state-redistribution/",
title: {
rendered: "The NSW 2020 State Redistribution"
},
content: {
rendered: " <figure class="wp-block-embed is-type-video is-provider-youtube
(etc.)
Create another Codable type as below and update Post,
struct Rendered: Codable {
var rendered: String
}
struct Post: Codable, Identifiable {
let id = UUID()
var title: Rendered
var content: Rendered
}

SwiftUI: heterogeneous collection of destination views for NavigationLink?

I’m building like a demo app of different examples, and I’d like the root view to be a List that can navigate to the different example views. Therefore, I tried creating a generic Example struct which can take different destinations Views, like this:
struct Example<Destination: View> {
let id: UUID
let title: String
let destination: Destination
init(title: String, destination: Destination) {
self.id = UUID()
self.title = title
self.destination = destination
}
}
struct Example1View: View {
var body: some View {
Text("Example 1!")
}
}
struct Example2View: View {
var body: some View {
Text("Example 2!")
}
}
struct ContentView: View {
let examples = [
Example(title: "Example 1", destination: Example1View()),
Example(title: "Example 2", destination: Example2View())
]
var body: some View {
List(examples, id: \.id) { example in
NavigationLink(destination: example.destination) {
Text(example.title)
}
}
}
}
Unfortunately, this results in an error because examples is a heterogeneous collection:
I totally understand why this is broken; I’m creating a heterogeneous array of examples because each Example struct has its own different, strongly typed destination. But I don’t know how to achieve what I want, which is an array that I can make a List out of which has a number of different allowed destinations.
I’ve run into this kind of thing in the past, and in the past I’ve gotten around it by wrapping my generic type and only exposing the exact properties I needed (e.g. if I had a generic type that had a title, I would make a wrapper struct and protocol that exposed only the title, and then made an array of that wrapper struct). But in this case NavigationLink needs to have the generic type itself, so there’s not a property I can just expose to it in a non-generic way.
You can use the type-erased wrapper AnyView. Instead of making Example generic, make the destination view inside of it be of type AnyView and wrap your views in AnyView when constructing an Example.
For example:
struct Example {
let id: UUID
let title: String
let destination: AnyView
init(title: String, destination: AnyView) {
self.id = UUID()
self.title = title
self.destination = destination
}
}
struct Example1View: View {
var body: some View {
Text("Example 1!")
}
}
struct Example2View: View {
var body: some View {
Text("Example 2!")
}
}
struct ContentView: View {
let examples = [
Example(title: "Example 1", destination: AnyView(Example1View())),
Example(title: "Example 2", destination: AnyView(Example2View()))
]
var body: some View {
NavigationView {
List(examples, id: \.id) { example in
NavigationLink(destination: example.destination) {
Text(example.title)
}
}
}
}
}

Resources