I am following tutorials to understand SwiftUI, and specifically how to call an API when a view appears.
I saw this:
List(results, id: \.trackId) { item in
ListRow(item)
}
.task {
// perform API here
}
But as my app targets iOS 14, I get this error:
'task(priority:_:)' is only available in iOS 15.0 or newer
So what could I do instead? Thank you for your help
You can write a version of task { } that works for iOS 13, iOS 14 and uses apple's version for iOS 15:
extension View {
#available(iOS, deprecated: 15.0, message: "This extension is no longer necessary. Use API built into SDK")
func task(priority: TaskPriority = .userInitiated, _ action: #escaping #Sendable () async -> Void) -> some View {
self.onAppear {
Task(priority: priority) {
await action()
}
}
}
}
async await is available for iOS 13+.
https://developer.apple.com/documentation/swift/task
if you need to use an async call is wrap the call in Task
.onAppear(){
Task{
//Your async code here
// await yourFuncHere()
}
}
.onAppear is a bit un reliable so I might opt for an init of an ObservableObject as an alternative.
Just switching to .onAppear is not correct since it's missing the point of Structured Concurrency. Every time you create a Task yourself you should be suspicious, you are doing something out of the ordinary.
Granted, in this case we don't have available a "structured concurrency aware" lifecycle modifier, so we need to make our own with Task init, but that means you need to be responsible of respecting structured concurrency!
This means that getting a proper backwards compatible solution to work is a bit more code, since you want to handle cancellation properly. For that you need to use also .onDisappear and cancel the task that you started on .onAppear.
If you want to have it reusable you can make a custom .task modifier.
import SwiftUI
struct ContentView: View {
#State var results = [TaskEntry]()
var body: some View {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.title)
}
// this one onAppear you can use it
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/todos") else {
print("Your API end point is Invalid")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let response = try? JSONDecoder().decode([TaskEntry].self, from: data) {
DispatchQueue.main.async {
self.results = response
}
return
}
}
}.resume()
}
}
Related
I am creating a game where, after a user signs in, I want to send their playerID to my backend. Since this is in SwiftUI, I have the following (btw I know we're not supposed to be using playerID anymore but this is just a minimal reproducible example):
import SwiftUI
import GameKit
struct SampleView: View {
let localPlayer = GKLocalPlayer.local
func authenticateUser() async {
localPlayer.authenticateHandler = { vc, error in
guard error == nil else {
print(error?.localizedDescription ?? "")
return
}
if localPlayer.isAuthenticated {
let playerID = localPlayer.playerID
GKAccessPoint.shared.isActive = localPlayer.isAuthenticated
// here is where I would like to make an async call
}
}
}
var body: some View {
VStack {
Text("Sample View")
}
.task {
await authenticateUser()
}
}
}
struct SampleView_Previews: PreviewProvider {
static var previews: some View {
SampleView()
}
}
In the comment indicating where I'd like to place an async call, I have tried something like
await myBackendCall(playerID)
but this throws the error
Invalid conversion from 'async' function of type '(UIViewController?, (any Error)?) async -> Void' to synchronous function type '(UIViewController?, (any Error)?) -> Void'
which makes sense given that the authenticateHandler function isn't an async function.
What is the best approach here? I'd like to wait until I have the value for PlayerID, and then call await myBackendCall(playerID). Any advice here would be much appreciated, thank you!
To make a completion handler async use a continuation, it returns true if the user is authenticated, otherwise false.
func authenticateUser() async -> Bool {
return await withCheckedContinuation { continuation in
localPlayer.authenticateHandler = { vc, error in
if let error {
print(error.localizedDescription)
continuation.resume(returning: false)
} else {
continuation.resume(returning: localPlayer.isAuthenticated)
}
}
}
}
and in the task scope write
.task {
let isAuthenticated = await authenticateUser()
if isAuthenticated {
let playerID = localPlayer.playerID
GKAccessPoint.shared.isActive = localPlayer.isAuthenticated
// here is where I would like to make an async call
}
}
When you have a callback closure (like authenticateHandler), it invariably means that the closure may possibly be called multiple times. The appropriate async-await pattern would be an AsyncSequence (e.g., an AsyncStream or an AsyncThrowingStream).
So, you might wrap authenticateHandler in an asynchronous sequence, like so:
func viewControllers() -> AsyncThrowingStream<UIViewController, Error> {
AsyncThrowingStream<UIViewController, Error> { continuation in
GKLocalPlayer.local.authenticateHandler = { viewController, error in
if let viewController {
continuation.yield(viewController)
} else {
continuation.finish(throwing: error ?? GKError(.unknown))
}
}
}
}
Then you could do things like:
.task {
do {
for try await _ in viewControllers() {
GKAccessPoint.shared.isActive = GKLocalPlayer.local.isAuthenticated
// do your subsequent `async` call here
}
} catch {
GKAccessPoint.shared.isActive = false
print(error.localizedDescription)
}
}
For more information, see WWDC 2021 video Meet AsyncSequence. But the idea is that withCheckedContinuation (or withThrowingCheckedContinuation) is designed for completion handler patterns, where it must be called once, and only once. If you use a checked continuation and the closure is called again, it will be “logging correctness violations”, because “You must call a resume method exactly once on every execution path throughout the program.”
Instead, in cases where it may be called multiple times, consider handling it as an asynchronous sequence.
I have been trying to deal with an #escaping closure in swift but I'm just not getting my head around it. My aim is to retrieve a download link from Google Firebase Storage - using the id field retrieved from a DB Document (originally) so I can download via NukeUI's LazyImage but I'm clearly missing something, basically is this the best way to approach this and if so what am I doing wrong here? If there is a more suitable approach to solve this can you point me in the right direction.
the code from the swiftUI view
var body: some View {
GeometryReader { geometry in
ScrollView {
let side = geometry.size.width / 4
let item = GridItem(.fixed(side), spacing: 2)
LazyVGrid(columns: Array(repeating: item, count: 4), spacing: 2) {
// Image Section.
ForEach(0..<siteData.sites.count, id: \.self) { index in
let currentSiteURL: String = grabURL(id: siteData.sites[index].id!)
LazyImage(source: currentSiteURL)
.frame(width: side, height: side)
.onAppear { model.onAppear(index) }
.onDisappear { model.onDisappear(index) }
}
}
}
}
}
And the function to retrieve the url to download:
func grabURL(id: String, completion: #escaping (String?)->Void) {
let ref = Storage.storage().reference().child("ImagesThumb")
ref.child("\(id).jpeg").downloadURL { (url, error) in
if let error = error {
print("Error Occured: \(error)")
completion(nil)
return
}
guard let siteURL = url else {
completion(nil)
return
}
completion(siteURL.absoluteString)
}
}
We use #escaping when the closure passed will outlive the parent function. Looking at your implementation, I think it's safe to say that your use of #escaping is right.
However, as vadian commented, your data fetching function is a bad practice. Consider using .task and async/await.
I am trying to implement a search bar in my app, as now I want to use the keyword typed in the search bar to make an API call to fetch backend data, here is my code:
struct SearchView: View {
#State private var searchText : String=""
#ObservedObject var results:getSearchList
init(){
results = SearchList(idStr: self.searchText)
}
var body: some View {
NavigationView {
VStack {
SearchBar(text: $searchText)
}.navigationBarTitle(Text("Search"))
}
}
}
I implement SearchBar view followed the this tutorial https://www.appcoda.com/swiftui-search-bar/ exactly,
and getSearchList is a class which has an var called idStr,
struct searchResEntry: Codable, Identifiable{
var id:Int
var comment:String
}
class SearchList: ObservableObject {
// 1.
#Published var todos = [searchResEntry]()
var idStr: String
init(idStr: String) {
self.idStr = idStr
let url = URL(string: "https://..." + idStr)!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let todoData = data {
// 3.
let decodedData = try JSONDecoder().decode([searchResEntry].self, from: todoData)
DispatchQueue.main.async {
self.todos = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
the problem I am struggling now is that I want to use the variable searchText to initialize the getSearchList , getSearchList has an var called idStr, this idStr is to used to store the typed keyword, my code always get an error: 'self' used before all stored properties are initialized , I have no idea how to deal with this.
Here is your code, edited by me:
struct SearchView: View {
#StateObject var results = SearchList()
var body: some View {
NavigationView {
VStack {
SearchBar(text: $results.searchText)
}.navigationBarTitle(Text("Search"))
}
}
}
struct SearchResEntry: Codable, Identifiable {
var id:Int
var backdrop_path:String
}
class SearchList: ObservableObject {
#Published var todos = [SearchResEntry]()
#Published var searchText: String = ""
var cancellable: AnyCancellable?
init() {
cancellable = $searchText.debounce(
for: .seconds(0.2),
scheduler: RunLoop.main
).sink { _ in
self.performSearch()
}
}
func performSearch() {
if let pathParam = searchText.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed),
let url = URL(string: "https://hw9node-310902.uc.r.appspot.com/mutisearch/\(pathParam)") {
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let todoData = data {
let decodedData = try JSONDecoder().decode([SearchResEntry].self, from: todoData)
DispatchQueue.main.async {
self.todos = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
} else {
print("Invalid URL")
}
}
}
Explanation
You are free to reverse the optional changes i made, but here are my explanations:
Use capital letter at the beginning of a Type's name. e.g write struct SearchResEntry, don't write struct searchResEntry. This is convention. Nothing big will happen if you don't follow conventions, but if anyone other than you (or maybe even you in 6 months) look at that code, chances are they go dizzy.
Dont start a Type's name with verbs like get! Again, this is just a convention. If anyone sees a getSomething() or even GetSomething() they'll think thats a function, not a Type.
Let the searchText be a published property in your model that performs the search. Don't perform search on init, instead use a function so you can initilize once and perform search any time you want (do results.performSearch() in your View). Also you can still turn your searchText into a binding to pass to your search bar (look at how i did it).
EDIT answer to your comment
I could right-away think of 3 different answers to your comment. This is the best of them, but also the most complicated one. Hopefully i chose the right option:
As you can see in the class SearchList i've added 2 things. First one is a cancellable to store an AnyCancellable, and second is the thing in init() { ... }. In init, we are doing something which results in an AnyCancellable and then we are storing that in the variable that i added.
What am i doing In init?
first $searchText gives us a Publisher. Basically, the publisher is called whenever the searchText value changes. Then you see .debounce(for: .seconds(0.2), on: RunLoop.main) which means only let the latest input go through and reach the next thing (the next thing is .sink { } as you can see), only if the user has stopped writing for 0.2 seconds. This is very helpful to avoid a load of requests to the server which can eventually make servers give you a 429 Too Many Requests error if many people are using your app (You can remove the whole .debounce thing if you don't like it). And the last thing is .sink { } which when any value reaches that point, it'll call the performSearch func for you and new results will be acquired from the server.
Alternative way
(again talking about your comment)
This is the simpler way. Do as follows:
remove init() { ... } completely if you've added it
remove var cancellable completely if you've added it
in your SearchView, do:
.onChange(of: results.searchText) { _ in
results.performSearch()
}
pretty self-explanatory; it'll perform the search anytime the searchText value is changed.
I have a SwiftUI app that fetches some information from the backend when the view appears and then attempts to update the State by setting #Published vars in an ObservableObject. The problem I have is it doesn't update at first fetch (it remains empty since it was initialized with an empty array) but if I click to another view and come back it's updated (since the information was already fetched).
Obviously, the intended thing I'm going for with using #Published is for the view to update once the information is fetched. This is part of a larger app but I have the reduced version of what I have below.
First, we have a parent view that contains the view I want to update.
struct ParentView: View {
var body: some View {
NavigationView {
ScrollView {
VStack {
SummaryView()
// In real life I have various forms of summary
// but to simplify here I will just use this one SummaryView.
SummaryView()
SummaryView()
}
}
}
}
}
Here is the summary view itself:
struct SummaryView: View {
#ObservedObject var model = AccountsSummaryViewModel()
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("Accounts")
.font(.title)
Spacer()
NavigationLink(
destination: AccountView(),
label: {
Image("RightArrow")
})
}
if model.accounts.count > 0 {
Divider()
}
// And if I add the following line for debugging
//Text(model.accounts.count)
// It remains 0.
ForEach(model.accounts, id: \.id) { account in
Text(account.account.text)
}
}
.padding()
.onAppear() {
model.onAppear()
}
}
}
Here is it's simple view model:
class AccountsSummaryViewModel: ObservableObject, Identifiable {
#Published var accounts: [AccountIdentifiable] = []
func onAppear() {
AccountsService.accounts { (success, error, response) in
DispatchQueue.main.async {
// This always succeeds
if let response = response {
// All AccountIdentifiable does is make a struct that is Identifiable (has an account and a var id = UUID())
self.accounts = Array(response.accounts.map { AccountIdentifiable(account: $0) }.prefix(3))
}
}
}
}
}
Here is the contents of the AccountsService also, I will note that the URL is a localhost but I'm not sure if that matters:
public struct AccountsService {
public static func accounts(completion: #escaping ((Bool, Error?, AccountsResponse?) -> Void)) {
guard let url = getAllAccountsURL() else {
completion(false, nil, nil)
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.allHTTPHeaderFields = ["Content-Type": "application/json",
BusinessConstants.SET_COOKIE : CredentialsObject.shared.jwt]
let task = URLSession.shared.dataTask(with: request) { (data, urlResponse, error) in
guard let data = data else {
completion(false, error, nil)
return
}
guard let response = try? JSONDecoder().decode(AccountsResponse.self, from: data) else {
completion(false, error, nil)
return
}
// This does successfully decode and return here.
completion(true, nil, response)
return
}
task.resume()
}
private static func getAllAccountsURL() -> URL? {
let address = "\(BusinessConstants.SERVER)/plaid/accounts"
return URL(string: address)
}
}
I have read that there are issues with an empty ScrollView, however, my ScrollView is never empty as I have those static text elements. I also read that if you use a ForEach without the id it can fail - but you can see I am using the id so I'm kind of at a loss.
I have print statements in the onAppear() so I know it runs and successfully sets the #Published accounts but looking at the UI and putting breakpoints in the ForEach I can see the view does not update. However, if I navigate somewhere else in my app, and then come back to the ParentView then since the #Published accounts is non-empty (already fetched) it updates perfectly.
It looks like you're running into a problem because of the two levels of observed objects, with model: AccountsSummaryViewModel containing accounts: [AccountIdentifiable].
SwiftUI will only watch one level, leading to your ParentView not updating when accounts is set more than one UI level down.
As discussed here, one option is to use PublishedObject via the Swift Package Manager in Xcode. Changing model in your SummaryView to #PublishedObject may be all that's required to fix this.
The reason it was not working was due to the fact that I was using #ObservedObject instead of #StateObject in SummaryView. Making the change fixed the issue.
I believe that I have set up my view and view model correctly. I have also confirmed that the network request returns data (via the console). I am confused on why my published property isn't updating my view with the fetched data.
Here is my view model:
class ProductViewModel: ObservableObject {
var didChange = PassthroughSubject<ProductViewModel, Never>()
#Published var mensProducts = [StripeProduct]()
init() {
}
func getMenItems() {
// hit the URL and
guard let url = URL(string: "http://127.0.0.1:3000/bags") else {
return
}
URLSession.shared.dataTask(with: url) { (data, response, err) in
// return the data asynchronously so that the call doesn't have to complete before loading the UI
DispatchQueue.main.async {
print("Decode data")
self.mensProducts = try! JSONDecoder().decode([StripeProduct].self, from: data!)
}
}
.resume()
}
}
Here is my view:
struct MenProducts: View {
#ObservedObject var productVM = ProductViewModel()
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack {
ForEach(self.productVM.mensProducts) { item in
ProductView(productID: item.productID, photo: "menMerch", price: item.price, name: item.productName, height: geometry.size.height/2, width: geometry.size.width)
}
}
}
}
.onAppear(perform: self.productVM.getMenItems)
}
}
First, I advise you to move your getMenItems() call to the init() method of ProductViewModel.
Then, you can remove the .onAppear(perform: self.productVM.getMenItems) in your MenProducts view and mark the method private in the ProductViewModel as no outside class/struct will be calling it.
I would also recommend you to not explicitly call the background queue with DispatchQueue.main.async as URLSession data task operations are asynchronous already.
You can read more about JSON decoding to Models in this great article: https://www.avanderlee.com/swift/json-parsing-decoding/