I have a list with data from the search.
to get data I want to call to await func (swift 5.5) but I get this error:
"Cannot pass function of type '() async -> ()' to parameter expecting
synchronous function type"
this is my code:
struct ContentView: View {
#ObservedObject var twitterAPI: TwitterAPI = TwitterAPI()
#State private var searchText = "TheEllenShow" // TheEllenShow
var body: some View {
NavigationView {
VStack{
if twitterAPI.twitterSearchResults?.resultDataVM != nil{
List {
ForEach((twitterAPI.twitterSearchResults?.resultDataVM)!) { item in
Text(item.text)
}
}
.refreshable {
await twitterAPI.executeQuery(userName: searchText)
}
}else{
Text("Loading")
}
Spacer()
}
.searchable(text: $searchText)
.onSubmit(of: .search) {
await twitterAPI.executeQuery(userName: searchText)
}
.navigationTitle("Twitter")
}
.task {
await twitterAPI.executeQuery(userName: searchText)
}
} }
To call asynchronous code from a synchronous code block, you can create a Task object:
.onSubmit(of: .search) {
Task {
await twitterAPI.executeQuery(userName: searchText)
}
}
You could bounce it over to .task like this:
#State var submittedSearch = ""
#State var results = []
.onSubmit(of: .search) {
submittedSearch = searchText
}
.task(id: submittedSearch) {
if submittedSearch.isEmpty {
return
}
results = await twitterAPI.executeQuery(userName: submittedSearch)
}
Has the advantage it will be cancelled and restarted if the search changes and also when the underlying UIView disappears.
Related
In a view, I want to wait for a series of async calls to finish loading, then redirect to another screen. Unfortunately, I see the code running in the back (The JSON data gets loaded) but once it completes it does not redirect to the new view.
Here is my view:
struct loadingView: View {
#ObservedObject var dataLoader: DataLoader = DataLoader()
#State var isLoaded: Bool = false
var body: some View {
VStack {
Text("Loading \(isLoaded)")
}
}
.task {
await self.dataloader.loadJSONData(isLoaded: $isLoaded)
MainScreen()
}
}
...and the DataLoader class:
#MainActor DataLoader: NSObject, ObservableObject {
func loadJSONData(isLoaded: Binding<Bool>) {
await doLoadData()
isLoaded.wrappedValue = True
}
func doLoadData() async {
/* do data load */
/* This code works */
}
}
"Redirecting" here doesn't really make sense. Do you really want the user to be able to navigate back to the loading screen? Perhaps you're thinking of this like a web page, but SwiftUI is nothing like that. What you really want to do is display one thing when loading, and a different thing when loaded. That's just if, not "redirection."
Instead, consider the following pattern. Create this kind of LoadingView (extracted from some personal code of mine):
struct LoadingView<Content: View, Model>: View {
enum LoadState {
case loading
case loaded(Model)
case error(Error)
}
#ViewBuilder let content: (Model) -> Content
let loader: () async throws -> Model
#State var loadState = LoadState.loading
var body: some View {
ZStack {
Color.white
switch loadState {
case .loading: Text("Loading")
case .loaded(let model): content(model)
case .error(let error): Text(verbatim: "Error: \(error)")
}
}
.task {
do {
loadState = .loaded(try await loader())
} catch {
loadState = .error(error)
}
}
}
}
It require no redirection. It just displays different things when in different states (obviously the Text view can be replaced by something more interesting).
Then to use this, embed it in another View. In my personal code, that includes a view like this:
struct DailyView: View {
var body: some View {
LoadingView() { model in
LoadedDailyView(model: model)
} loader: {
try await DailyModel()
}
}
}
Then LoadedDailyView is the "real" view. It is handled a fully populated model that is created by DailyModel.init (a throwing, async init).
You could try this approach, using NavigationStack and NavigationPath to Redirecting after task w/ Await completes.
Here is the code I use to test my answer:
struct ContentView: View {
var body: some View {
loadingView()
}
}
#MainActor
class DataLoader: NSObject, ObservableObject {
func loadJSONData() async {
await doLoadData()
// for testing, wait for 1 second
try? await Task.sleep(nanoseconds: 1 * 1_000_000_000)
}
func doLoadData() async {
/* do data load */
/* This code works */
}
}
struct loadingView: View {
#StateObject var dataLoader = DataLoader()
#State private var navPath = NavigationPath()
var body: some View {
NavigationStack(path: $navPath) {
VStack (spacing: 44) {
Text("Loading....")
}
.navigationDestination(for: Bool.self) { _ in
MainScreen()
}
}
.task {
await dataLoader.loadJSONData()
navPath.append(true)
}
}
}
struct MainScreen: View {
var body: some View {
Text("---> MainScreen here <---")
}
}
If you need ios 15 or earlier, then use NavigationView:
struct loadingView: View {
#StateObject var dataLoader = DataLoader()
#State var isLoaded: Bool?
var body: some View {
NavigationView {
VStack {
Text(isLoaded == nil ? "Loading..." : "Finished loading")
NavigationLink("", destination: MainScreen(), tag: true, selection: $isLoaded)
}
}.navigationViewStyle(.stack)
.task {
await dataLoader.loadJSONData()
isLoaded = true
}
}
}
If your loadingView has the only purpose of showing the "loading" message, then
display the MainScreen after the data is loaded, you could use the following approach using a simple swicth:
struct loadingView: View {
#StateObject var dataLoader = DataLoader()
#State private var isLoaded = false
var body: some View {
VStack {
if isLoaded {
MainScreen()
} else {
ProgressView("Loading")
}
}
.task {
await dataLoader.loadJSONData()
isLoaded = true
}
}
}
Use #StateObject instead of #ObservedObject. Use #Published instead of trying to pass a binding to the object (that is a mistake because a binding is just a pair of get and set closures that will expire if LoadingView is re-init), use Group with an if to conditionally show a View e.g.
struct LoadingView: View {
#StateObject var dataLoader: DataLoader = DataLoader()
var body: some View {
Group {
if dataLoader.isLoaded {
LoadedView(data: dataLoader.data)
} else {
Text("Loading...")
}
}
.task {
await dataloader.loadJSONData()
}
}
The DataLoader should not be #MainActor because you want it to run on a background thread. Use #MainActor instead on a sub-task once the async work has finished e.g.
class DataLoader: ObservableObject {
#Published var isLoaded = false
#Published var data: [Data] = []
func loadJSONData async {
let d = await doLoadData()
Task { #MainActor in
isLoaded = true
data = d
}
}
func doLoadData() async {
/* do data load */
/* This code works */
}
}
This pattern is shown in Apple's tutorial here, PandaCollectionFetcher.swift copied below:
import SwiftUI
class PandaCollectionFetcher: ObservableObject {
#Published var imageData = PandaCollection(sample: [Panda.defaultPanda])
#Published var currentPanda = Panda.defaultPanda
let urlString = "http://playgrounds-cdn.apple.com/assets/pandaData.json"
enum FetchError: Error {
case badRequest
case badJSON
}
func fetchData() async
throws {
guard let url = URL(string: urlString) else { return }
let (data, response) = try await URLSession.shared.data(for: URLRequest(url: url))
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.badRequest }
Task { #MainActor in
imageData = try JSONDecoder().decode(PandaCollection.self, from: data)
}
}
}
So, i'm creating a simple login page where an async call is made passing the credentials and with a successful response, it should navigate to another view. The call is working just fine, i've managed to receive the correct response, but i can't get the navigationDestination(isPresented:destination:) to work. Any clues on what i'm missing here?
LoginView:
struct LoginView: View {
#StateObject private var loginViewModel = LoginViewModel()
#State var success: Bool = false
var body: some View {
VStack {
if loginViewModel.isBusy {
ProgressView()
} else {
NavigationStack {
TextField("Username", text: $loginViewModel.username)
SecureField("Password", text: $loginViewModel.password)
Button("Login") {
Task {
success = await loginViewModel.login()
}
}
.navigationDestination(isPresented: $success) {
HomeView()
}
}
.buttonStyle(PlainButtonStyle())
}
}
}
}
LoginViewModel:
#MainActor
class LoginViewModel: ObservableObject {
#Published var response: LoginResponse?
#Published var isBusy: Bool = false
var username = ""
var password = ""
func login() async -> Bool {
isBusy = true
do {
response = try await Service().login(username: username, password: password)
isBusy = false
return true
} catch {
isBusy = false
print(error.localizedDescription)
return false
}
}
}
Im learning swift and this error/warning is driving me crazy because I cant see what call Im making that causing it... The Xcode warning only shows up in my #main struct
Modifying state during view update, this will cause undefined behavior.
I thought it might be in the ListView, but I realized the warning only shows after the "Submit Post" button is it.
Im looking for a fix, but more importantly and explanation as to why this is happening and the proper usage moving forward.
import SwiftUI
import Firebase
#main
struct SocialcademyApp: App {
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
PostsList()
}
}
}
struct PostsList: View {
#StateObject var viewModel = PostsViewModel()
#State private var searchText = ""
#State private var showNewPostForm = false
var body: some View {
NavigationView {
List(viewModel.posts) { post in
if searchText.isEmpty || post.contains(searchText) {
PostRow(post: post)
}
}
.searchable(text: $searchText)
.navigationTitle("Posts")
.toolbar {
Button {
showNewPostForm = true
} label: {
Label("New Post", systemImage: "square.and.pencil")
}
}
.sheet(isPresented: $showNewPostForm) {
NewPostView(creationAction: viewModel.makeCreationAction())
}
}
}
}
struct NewPostView: View {
typealias CreationAction = (Post) async throws -> Void
let creationAction: CreationAction
#State private var post = Post(title: "", content: "", authorName: "")
#State private var state = FormState.idle
#Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
Form {
Section {
TextField("Title", text: $post.title)
TextField("Author Name", text: $post.authorName)
}
Section {
TextField("Content", text: $post.content)
.multilineTextAlignment(.leading)
}
Button(action: createPost, label: {
if state == .working {
ProgressView() } else {
Text("Submit Post")
}
})
.font(.headline)
.frame(maxWidth: .infinity)
.foregroundColor(.white)
.padding()
.listRowBackground(Color.accentColor)
}
}
.navigationTitle("New Post")
.disabled(state == .working)
.alert("Cannot Create Post", isPresented: $state.isError, actions: {}) {
Text("Sorry, something went wrong")
}
.onSubmit {
createPost()
}
}
private func createPost() {
print("[NewPostForm] creating a new post")
Task {
state = .working
do {
try await creationAction(post)
dismiss()
} catch {
state = .error
print("[NewPostForm] Cannot create post: \(error)")
}
}
}
}
private extension NewPostView {
enum FormState {
case idle, working, error
var isError: Bool {
get {
self == .error
}
set {
guard !newValue else { return }
self = .idle
}
}
}
}
#MainActor
class PostsViewModel: ObservableObject {
#Published var posts = [Post.testPost]
func makeCreationAction() -> NewPostView.CreationAction {
return { [weak self] post in
try await PostsRepository.create(post)
self?.posts.insert(post, at: 0)
}
}
}
I want to rename an item in a ForEach list. When i try to load the EditListView for a selected list the entire app crashes.
This is a SwiftUI macOS app and the items are saved using CoreData.
The crash happens as soon as you click on "Edit List" for any of the list items.
It doesn't crash if i remove this view model var listVM: MyListViewModel from the EditListViewModel.
Here's the EditListView
struct EditListView: View {
let name: String
#Binding var isVisible: Bool
var list: MyListViewModel
#ObservedObject var editListVM: EditListViewModel
init(name: String,list: MyListViewModel, isVisible: Binding<Bool> ) {
self.list = list
editListVM = EditListViewModel(listVM: list)
_isVisible = isVisible
self.name = name
}
var body: some View {
VStack {
Text(name)
Button(action: {
editListItemVM.update()
}) {
Text("Update List Name")
}
Button(action: {
self.isVisible = false
}) {
Text("Cancel")
}
}......
EditListViewModel
class EditListViewModel: ObservableObject {
var listVM: MyListViewModel
#Published var name: String = ""
init(listVM: MyListViewModel) {
self.listVM = listVM
name = listVM.name
}
func update(){
....}
}
MyListViewModel
struct MyListViewModel: Identifiable {
private let myList: MyList
init(myList: MyList) {
self.myList = myList
}
var id: NSManagedObjectID {
myList.objectID
}
var name: String {
myList.name ?? ""
}
}
MyList Model
#objc(MyList)
public class MyList: NSManagedObject, BaseModel {
static var all: NSFetchRequest<MyList> {
let request: NSFetchRequest<MyList> = MyList.fetchRequest()
request.sortDescriptors = []
return request
}
}
extension MyList {
#nonobjc public class func fetchRequest() -> NSFetchRequest<MyList> {
return NSFetchRequest<MyList>(entityName: "MyList")
}
#NSManaged public var name: String?
}
extension MyList : Identifiable {
}
Here's the Main View
struct MyListsView: View {
#StateObject var vm: MyListsViewModel
#State private var showPopover: Bool = false
init(vm: MyListsViewModel) {
_vm = StateObject(wrappedValue: vm)
}
List {
Text("My Lists")
ForEach(vm.myLists) { myList in
NavigationLink {
MyListItemsHeaderView(name: myList.name)
.sheet(isPresented: $showPopover) {
EditListView(name: myList.name, list: MyListViewModel(myList: MyList()), isVisible: $showPopover)
}
}
}.contextMenu {
Button {
showPopover = true
// Show the EditListView
} label: {
Label("Edit List", systemImage: "pen.circle")
}......
First get rid of your view model objects we don't use those in SwiftUI. We use the View struct and the property wrappers like #FetchRequest make the struct behave like an object. It looks like this:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
ItemView(item: item)
I recommend looking at Xcode's app template with core data checked.
Then for editing you can use .sheet(item: like this:
struct ItemEditor: View {
#ObservedObject var item: Item // this is the scratch pad item
#Environment(\.managedObjectContext) private var context
#Environment(\.dismiss) private var dismiss // causes body to run
let onSave: () -> Void
#State var errorMessage: String?
var body: some View {
NavigationView {
Form {
Text(item.timestamp!, formatter: itemFormatter)
if let errorMessage = errorMessage {
Text(errorMessage)
}
Button("Update Time") {
item.timestamp = Date()
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
// first save the scratch pad context then call the handler which will save the view context.
do {
try context.save()
errorMessage = nil
onSave()
} catch {
let nsError = error as NSError
errorMessage = "Unresolved error \(nsError), \(nsError.userInfo)"
}
}
}
}
}
}
}
struct ItemEditorConfig: Identifiable {
let id = UUID()
let context: NSManagedObjectContext
let item: Item
init(viewContext: NSManagedObjectContext, objectID: NSManagedObjectID) {
// create the scratch pad context
context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context.parent = viewContext
// load the item into the scratch pad
item = context.object(with: objectID) as! Item
}
}
struct EditItemButton: View {
let itemObjectID: NSManagedObjectID
#Environment(\.managedObjectContext) private var viewContext
#State var itemEditorConfig: ItemEditorConfig?
var body: some View {
Button(action: edit) {
Text("Edit")
}
.sheet(item: $itemEditorConfig, onDismiss: didDismiss) { config in
ItemEditor(item: config.item) {
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
itemEditorConfig = nil // dismiss the sheet
}
.environment(\.managedObjectContext, config.context)
}
}
func edit() {
itemEditorConfig = ItemEditorConfig(viewContext: viewContext, objectID: itemObjectID)
}
func didDismiss() {
// Handle the dismissing action.
}
}
struct ItemView: View {
#ObservedObject var item: Item
var body: some View {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditItemButton(itemObjectID: item.objectID)
}
}
}
}
the params for EditListView in the main view were incorrect.
Fixed it with the following params:
.sheet(isPresented: $showPopover) {
EditListView(name: myList.name, list: myList, isVisible: $showPopover)
}
I have a database with several objects with booleans as attribute. I'm looking for a function to invert all boolean objects when I press a button. I tried this function but several errors are displayed like (Value of type 'Bool' has no member 'indices') :
struct ViewList: View {
#Environment(\.managedObjectContext) var context
#State var newName: String = ""
#FetchRequest(
entity: Product.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Product.name, ascending: true)]
) var list: FetchedResults<Product>
var body: some View {
VStack {
HStack {
TextField("I insert the name of the product", text: $newName)
Button(action: { self.add()
self.newName = ""
})
{ Image(systemName: "plus") }
}
List {
ForEach(list, id: \.self) {
product in ViewItem(product: product)
}
}
}
}
public func add() {
let newProduct = Product(context: context)
newProduct.name = newName
do {
try context.save()
} catch {
print(error)
}
}
}
struct ViewItem: View {
#State var product: Product
#State var refresh: Bool = false
var body: some View {
NavigationLink(destination: ViewDetail(product: product, refresh: $refresh)) {
HStack(alignment: .top) {
Button( action: {
self.clean()
self.product.isSelected.toggle()
}) {
if self.product.isSelected == true {
Image(systemName: "checkmark")
} else {
Image(systemName: "checkmark").colorInvert()
}
}
VStack() {
Text(product.name)
if product.password != "" {
Text("Password : " + product.password)
}
Text(String(refresh)).hidden()
}
}
}
.onAppear {
self.refresh = false
}
}
}
I've been thinking about it, but I don't know how to go about it...
func clean() {
for( index ) in self.product.isSelected.indices {
self.product[index]isSelected = false
}
}
You need to create a query to flip the state of the isSelected flag. This logic is best kept out of the view system so you can use it anywhere.
You create a SelectionHandler
import CoreData
class SelectionHandler {
func clearSelection(in context: NSManagedObjectContext) {
for item in currentSelected(in: context) {
item.isSelected = false
}
}
func selectProduct(_ product: Product) {
guard let context = product.managedObjectContext else {
assertionFailure("broken !")
return
}
clearSelection(in: context)
product.isSelected = true
}
func currentSelected(in context: NSManagedObjectContext) -> [Product] {
let request = NSFetchRequest<Product>(entityName: Product.entity().name!)
let predicate = NSPredicate(format: "isSelected == YES")
request.predicate = predicate
do {
let result = try context.fetch(request)
return result
} catch {
print("fetch error =",error)
return []
}
}
}
which you can then use to select your desired product.
SelectionHandler().selectProduct(product)
As it stands your NavigationLink will do nothing because the parent list is not held in a NavigationView so you'll need to change the body of ViewList to look like this.
var body: some View {
NavigationView {
VStack {
HStack {
TextField("Create product with name", text: $newName)
Button(action: {
self.add()
self.newName = ""
})
{ Image(systemName: "plus") }
}
.padding()
List {
ForEach(list, id: \.self) { product in
ViewItem(product: product)
}
}
}
}
}
and in ViewItem , Product should be an ObservedObject so that changes are detected in the managedObject.
struct ViewItem: View {
#ObservedObject var product: Product
#State var refresh: Bool = false
var checkmarkImage: some View {
return Group {
if self.product.isSelected {
Image(systemName: "checkmark")
} else {
Image(systemName: "checkmark").colorInvert()
}
}
}
var body: some View {
NavigationLink(destination: ViewDetail(product: product, refresh: $refresh)) {
HStack {
checkmarkImage
Text(product.name ?? "wat")
}
}
}
}
The original Button won't play with the NavigationLink but you can simply apply the selection to onAppear in ViewDetail
struct ViewDetail: View {
#ObservedObject var product: Product
#Binding var refresh: Bool
var body: some View {
VStack {
Text("Hello, World!")
Text("Product is \(product.name ?? "wat")")
}
.onAppear {
SelectionHandler().selectProduct(self.product)
}
}
}