TabView blank when trying to display json data via onAppear() - swift

I followed this tutorial to call data from my API. I veered off a bit and instead used TabView to show a "home page" where data loads in the first tab. It "works" in the sense that if I navigate to another tab and go back to the home tab the data appears. When I open the app though, the tab is blank. I initially declare posts as an empty array, by why is onAppear() not populating it?
Here's the view that is supposed to be displaying my data
struct DiscoverView: View {
#ObservedObject var discoverPosts: DiscoverPosts
var body: some View {
NavigationView{
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .center){
ForEach(self.discoverPosts.posts) { post in
HStack{
DiscoverList(isModal : false,displayName : post.displayName,id : post.id,likes : post.likes,owner : post.owner,avatar : post.avatar,author_id : post.author_id,icebreaker : post.icebreaker,answer : post.answer,mediaLink : post.mediaLink,songURL : post.songURL,type : post.type,liked: post.liked)
}
.padding(10)
}
}
}
.onAppear(){
// self.discoverPosts.getPosts()
}
.navigationViewStyle(StackNavigationViewStyle())
.navigationBarTitle("Discover")
}
}
}
here is my discoverPosts()
class discoverPosts : ObservableObject {
#State var posts : [Post] = []
func getPosts(completion: #escaping ([Post]) -> ()){
let feedURL = "URL"
guard let url = URL(string: feedURL) else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
let posts = try! JSONDecoder().decode([Post].self, from: data!)
DispatchQueue.main.async {
self.posts = posts
completion(posts)
}
}
.resume()
}
}
my ConventView.swift that shows the TabView. I believe the issue could be the hierarchy
struct ContentView: View {
var body: some View {
Home()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct Home : View {
#State var show = false
#State var status = UserDefaults.standard.value(forKey: "status") as? Bool ?? false
var body: some View{
VStack{
if self.status{
TabView {
DiscoverView(discoverPosts: DiscoverPosts())
.tabItem(){
Image(systemName: "person")
.font(.system(size:20))
}
.tag(1)
InboxView(offsetLine: IndexSet.Element())
.tabItem(){
Image(systemName: "message")
.font(.system(size: 20))
}
.tag(2)
ProfileView()
.tabItem(){
Image(systemName: "person")
.font(.system(size: 20))
}
.tag(3)
}
.accentColor(Color.purple)
.navigationViewStyle(StackNavigationViewStyle())
}
else{
ZStack{
NavigationLink(destination: SignUp(show: self.$show), isActive: self.$show) {
Text("")
}
.hidden()
Login(show: self.$show)
}
}
}
.navigationBarTitle("")
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
.onAppear {
NotificationCenter.default.addObserver(forName: NSNotification.Name("status"), object: nil, queue: .main) { (_) in
self.status = UserDefaults.standard.value(forKey: "status") as? Bool ?? false
}
}
}
}
For those that experience this, You can throw Text("").frame(width: UIScreen.main.bounds.width) at the bottom of ScrollView

Change your ObservableObject to:
class DiscoverPosts: ObservableObject { // make Types capitalised
#Published var posts: [Post] = [] // <- replace #State with #Published
init() {
getPosts()
}
func getPosts() { // <- no need for completion handler, update `self.posts`
let feedURL = "URL"
guard let url = URL(string: feedURL) else { return }
URLSession.shared.dataTask(with: url) { data, _, _ in
let posts = try! JSONDecoder().decode([Post].self, from: data!)
DispatchQueue.main.async {
self.posts = posts
}
}
.resume()
}
}
and use it in your view like this:
struct DiscoverView: View {
#ObservedObject var discoverPosts: DiscoverPosts // declare only
var body: some View {
ZStack {
...
}
//.onAppear { // <- remove onAppear
// self.discoverPosts.getPosts()
//}
}
}
You also need to pass DiscoverPosts to DiscoverView from Home view:
DiscoverView(discoverPosts: DiscoverPosts())
Note that if you previously accessed self.posts in your view, you will now need to access self.discoverPosts.posts

Related

SWIFTUI Observable Object Data Task only runs once?

I have an observable object class that downloads an image from a url to display:
class ImageLoader : ObservableObject {
var didChange = PassthroughSubject<Data, Never>()
var data = Data() {
didSet {
didChange.send(data)
}
}
init(urlString:String){
guard let url = URL(string: urlString) else {return}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
self.data = data
print("imageloader1")
}
}
task.resume()
}
and I show it using:
struct ShowImage1: View {
#ObservedObject var imageLoader:ImageLoader
#State var image:UIImage = UIImage()
init(withURL url:String) {
imageLoader = ImageLoader(urlString:url)
}
var body: some View {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.edgesIgnoringSafeArea(.top)
.onReceive(imageLoader.didChange) {
data in self.image = UIImage(data: data) ?? UIImage()
}
}
The problem I'm having is this is only capable of running once, If i click off the ShowImage1 view and then click back on to it, ImageLoader doesn't run again, and I'm left with a blank page.
How can I ensure that ImageLoader Runs every time the ShowImage1 view is accessed?
EDIT:
I access ShowImage1 like this:
struct PostCallForm: View {
var body: some View {
NavigationView {
Form {
Section {
Button(action: {
if true {
self.showImage1 = true
}
}){
Text("View Camera 1 Snapshot")
}.overlay(NavigationLink(destination: ShowImage1(withURL: "example.com/1.jpg"), isActive: self.$showImage1, label: {
EmptyView()
}))
}
}
Section {
Button(action: {
}){
Text("Submit")
}
}
}.disabled(!submission.isValid)
}
}
}
import SwiftUI
import Combine
class ImageLoader : ObservableObject {
var didChange = PassthroughSubject<Data, Never>()
var data = Data() {
didSet {
didChange.send(data)
}
}
func loadImage(urlString:String) {
guard let url = URL(string: urlString) else {return}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
self.data = data
print("imageloader1")
}
}
task.resume()
}
}
struct ShowImage1Parent: View {
#State var url: String = ""
var sampleURLs: [String] = ["https://image.shutterstock.com/image-vector/click-here-stamp-square-grunge-600w-1510095275.jpg", "https://image.shutterstock.com/image-vector/certified-rubber-stamp-red-grunge-600w-1423389728.jpg", "https://image.shutterstock.com/image-vector/sample-stamp-square-grunge-sign-600w-1474408826.jpg" ]
var body: some View {
VStack{
Button("load-image", action: {
url = sampleURLs.randomElement()!
})
ShowImage1(url: $url)
}
}
}
struct ShowImage1: View {
#StateObject var imageLoader:ImageLoader = ImageLoader()
#State var image:UIImage = UIImage()
#Binding var url: String
var body: some View {
VStack{
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.edgesIgnoringSafeArea(.top)
.onReceive(imageLoader.didChange) {
data in self.image = UIImage(data: data) ?? UIImage()
}
.onChange(of: url, perform: { value in
imageLoader.loadImage(urlString: value)
})
}
}
}

About swift5 and alamofire5,how to use AF.request to check if login sucessed or failed?

I want to use AF.request to implement login, this is my code:
import Alamofire
var loginResult: Bool = false
func login() -> Bool {
let parameters: [String: String] = [
"password": "Adgj!4567",
"username": "admin",
]
var a = AF.request("http://192.168.64.2/logins.php", method: .post, parameters:parameters,encoder: URLEncodedFormParameterEncoder(destination: .httpBody)).response{
response in
if let data = response.data {
let result = String(data: data, encoding: .utf8)!
if result.contains("Login Success!"){
print (result)
loginResult = true
}else {
loginResult = false
}
}
}
return loginResult
}
When I called login(), I got response like this:
send request worked, but loginResult has been always false, and I know that because AF is async, my problem is that I want to check if loginResult's value is true and go to another page, but it is always false, what should i do???
ContentView.swift like this:
import SwiftUI
struct ContentView: View {
#State private var isLoginValid: Bool = false
#State private var shouldShowLoginAlert: Bool = false
var body: some View {
NavigationView{
VStack{
HStack(alignment: .center) {
VStack {
Label("username", systemImage: "")
Label("password", systemImage: "")
}
VStack {
TextField("input username", text: /*#START_MENU_TOKEN#*//*#PLACEHOLDER=Value#*/.constant("")/*#END_MENU_TOKEN#*/)
TextField("input password", text: /*#START_MENU_TOKEN#*//*#PLACEHOLDER=Value#*/.constant("")/*#END_MENU_TOKEN#*/)
}
}
.padding()
NavigationLink(destination: infoInputView(),isActive: self.$isLoginValid) {
Text("Login")
.onTapGesture {
if login(){
self.isLoginValid = true
}
else{
self.shouldShowLoginAlert = true
}
}
}.buttonStyle(BorderlessButtonStyle())
.navigationBarTitle("Login Screen")
.alert(isPresented: $shouldShowLoginAlert) {
Alert(title: Text("Email/Password incorrect"))
}
.padding(.all, 50.0)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
My mother tongue is not english, I hope you can understand me.
i have got a wrong way,the right way is this:
import SwiftUI
import Alamofire
struct ContentView: View {
#State private var isLoginValid: Bool = false
#State private var shouldShowLoginAlert: Bool = false
#State private var loginResult: Bool = false
var body: some View {
NavigationView{
VStack{
HStack(alignment: .center) {
VStack {
Label("用户名", systemImage: "")
Label("密码", systemImage: "")
}
VStack {
TextField("请输入用户名", text: /*#START_MENU_TOKEN#*//*#PLACEHOLDER=Value#*/.constant("")/*#END_MENU_TOKEN#*/)
TextField("请输入密码", text: /*#START_MENU_TOKEN#*//*#PLACEHOLDER=Value#*/.constant("")/*#END_MENU_TOKEN#*/)
}
}
.padding()
NavigationLink(destination: infoInputView(),isActive: self.$isLoginValid) {
Text("Login")
.onTapGesture {
login()
}
}.buttonStyle(BorderlessButtonStyle())
.navigationBarTitle("Login Screen")
.alert(isPresented: $shouldShowLoginAlert) {
Alert(title: Text("Email/Password incorrect"))
}
.padding(.all, 50.0)
}
}
}
func login() {
let parameters: [String: String] = [
"password": "Adgj!4567",
"username": "admin",
]
var a = AF.request(url, method: .post, parameters:parameters,encoder: URLEncodedFormParameterEncoder(destination: .httpBody))
a.response{
response in
if let data = response.data {
let result = String(data: data, encoding: .utf8)!
if result.contains("Login Success!") {
isLoginValid = true
}else {
shouldShowLoginAlert = true
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}

SwiftUI: View Model does not update the View

I try to implement a Search Bar with Algolia, and I use the MVVM pattern.
Here's my View Model:
class AlgoliaViewModel: ObservableObject {
#Published var idList = [String]()
func searchUser(text: String){
let client = SearchClient(appID: "XXX", apiKey: "XXX")
let index = client.index(withName: "Users")
let query = Query(text)
index.search(query: query) { result in
if case .success(let response) = result {
print("Response: \(response)")
do {
let hits: Array = response.hits
var idList = [String]()
for x in hits {
idList.append(x.objectID.rawValue)
}
DispatchQueue.main.async {
self.idList = idList
print(self.idList)
}
}
catch {
print("JSONSerialization error:", error)
}
}
}
}
}
Here is my View :
struct NewChatView : View {
#State private var searchText = ""
#ObservedObject var viewModel = AlgoliaViewModel()
var body : some View{
VStack(alignment: .leading){
Text("Select To Chat").font(.title).foregroundColor(Color.black.opacity(0.5))
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 12){
HStack {
TextField("Start typing",
text: $searchText,
onCommit: { self.viewModel.searchUser(text: self.searchText) })
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
self.viewModel.searchUser(text: self.searchText)
}) {
Image(systemName: "magnifyingglass")
}
} .padding()
List {
ForEach(viewModel.idList, id: \.self){ i in
Text(i)
}
}
}
}
}.padding()
}
}
I often use this pattern with Firebase and everything works fine, but here with Algolia the List remains empty in the NewChatView.
The print(self.idList) statement inside the View-Model shows the right idList, but it does not update the List inside the NewChatView.
You first need to create your own custom Identifiable and Hashable model to display the searchValue in a List or ForEach.
Something like this:
struct MySearchModel: Identifiable, Hashable {
let id = UUID().uuidString
let searchValue: String
}
Then use it in your AlgoliaViewModel. Set a default value of an empty array.
You can also map the hits received and convert it to your new model. No need for the extra for loop.
class AlgoliaViewModel: ObservableObject {
#Published var idList: [MySearchModel] = []
func searchUser(text: String) {
let client = SearchClient(appID: "XXX", apiKey: "XXX")
let index = client.index(withName: "Users")
let query = Query(text)
index.search(query: query) { result in
if case .success(let response) = result {
print("Response: \(response)")
do {
let hits: Array = response.hits
DispatchQueue.main.async {
self.idList = hits.map({ MySearchModel(searchValue: $0.objectID.rawValue) })
print(self.idList)
}
}
catch {
print("JSONSerialization error:", error)
}
}
}
}
}
For the NewChatView, you can remove the ScrollView as it conflicts with the elements inside your current VStack and would hide the List with the results as well. The following changes should display all your results.
struct NewChatView : View {
#State private var searchText = ""
#ObservedObject var viewModel = AlgoliaViewModel()
var body: some View{
VStack(alignment: .leading) {
Text("Select To Chat")
.font(.title)
.foregroundColor(Color.black.opacity(0.5))
VStack {
HStack {
TextField("Start typing",
text: $searchText,
onCommit: { self.viewModel.searchUser(text: self.searchText)
})
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
self.viewModel.searchUser(text: self.searchText)
}) {
Image(systemName: "magnifyingglass")
}
} .padding()
List {
ForEach(viewModel.idList) { i in
Text(i.searchValue)
.foregroundColor(Color.black)
}
}
}
}.padding()
}
}

ListView in child view is not refreshed correctly

There is a ListView. I make a transaction in Cloud Firestore by changing the field of an element when I click on it in the list. Data in the database changes as it should, but after this action all the elements in the list disappear (although there is .onAppear {fetchData}). An important point: this is a child view, there is no such problem in the parent view.
I also added a button at the bottom of the list to execute fetchData (), when I click on it, the data returns to the list
What could be the problem? Thanks
import SwiftUI
struct SecondView: View {
#ObservedObject var viewModel = BooksViewModel()
var body: some View {
VStack {
List(viewModel.books) { book in
VStack(alignment: .leading) {
Button("Update data"){
let updBook = book
self.viewModel.myTransaction(book: updBook)
}
Text(book.title)
.font(.headline)
Text(book.author)
.font(.subheadline)
Text("\(book.numberOfPages) pages")
.font(.subheadline)
}
}
.navigationBarTitle("Books")
.onAppear() {
self.viewModel.fetchData()
}
Button("update list"){
self.viewModel.fetchData()
}
}
}
}
ViewModel:
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
class BooksViewModel: ObservableObject {
#Published var books = [Book]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("books").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.books = documents.compactMap { queryDocumentSnapshot -> Book? in
return try? queryDocumentSnapshot.data(as: Book.self)
}
}
}
func deleteBook(book: Book){
if let bookID = book.id{
db.collection("books").document(bookID).delete()
}
}
func updateBook(book: Book) {
if let bookID = book.id{
do {
try db.collection("books").document(bookID).setData(from: book) }
catch {
print(error)
}
}
}
func addBook(book: Book) {
do {
let _ = try db.collection("books").addDocument(from: book)
}
catch {
print(error)
}
}
func myTransaction(book: Book){
let bookID = book.id
let targetReference = db.collection("books").document(bookID!)
db.runTransaction({ (transaction, errorPointer) -> Any? in
let targetDocument: DocumentSnapshot
do {
try targetDocument = transaction.getDocument(targetReference)
} catch let fetchError as NSError {
errorPointer?.pointee = fetchError
return nil
}
guard let oldValue = targetDocument.data()?["pages"] as? Int else {
let error = NSError(
domain: "AppErrorDomain",
code: -1,
userInfo: [
NSLocalizedDescriptionKey: "Unable to retrieve population from snapshot \(targetDocument)"
]
)
errorPointer?.pointee = error
return nil
}
// Note: this could be done without a transaction
// by updating the population using FieldValue.increment()
transaction.updateData(["pages": oldValue + 1], forDocument: targetReference)
return nil
}) { (object, error) in
if let error = error {
print("Transaction failed: \(error)")
} else {
print("Transaction successfully committed!")
}
}
}
}
Parent view:
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = BooksViewModel()
var body: some View {
NavigationView {
VStack {
List(viewModel.books) { book in
VStack(alignment: .leading) {
Button("Update"){
let delBook = book
self.viewModel.myTransaction(book: delBook)
}
Text(book.title)
.font(.headline)
Text(book.author)
.font(.subheadline)
Text("\(book.numberOfPages) pages")
.font(.subheadline)
}
}
.navigationBarTitle("Books")
.onAppear() {
self.viewModel.fetchData()
}
NavigationLink(destination: SecondView()){
Text("Second View")
}
}
}
}
}
A possible solution might be that your Views and its ViewModels interfere with each other. It looks like you create two instances of the same BookViewModel:
struct ContentView: View {
#ObservedObject var viewModel = BooksViewModel()
struct SecondView: View {
#ObservedObject var viewModel = BooksViewModel()
Try creating one BooksViewModel and pass it between views (you can use an #EnvironmentObject).

How to initialize data in Preview provider in SwiftUI

I am trying to fetch data from localhost, make a list of posts with List View and pass data to CustomDetailView. Here is my code for NetworkManager:
My ListView:
And StoryDetails View:
So what I have to pass to StoryDeatils_Preview?
Here is the StoryDetails code
import SwiftUI
struct StoryDetails: View {
var story: Story
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("story #123456")
.font(.callout)
.foregroundColor(Color.gray)
Spacer()
Text("5 days ago")
.font(.callout)
.foregroundColor(Color.gray)
Button(action:{
print("Hello there")
}){
Image(systemName:"info.circle").resizable()
.frame(width:22.0, height:22.0)
.accentColor(Color.gray)
}
}
Text(story.body)
.foregroundColor(.black)
.kerning(1)
.lineLimit(nil)
HStack {
Button(action: {
print("Hello World")
}){
HStack {
Image(systemName:"heart")
.accentColor(.black)
Text("233")
.foregroundColor(.black)
}
.padding(.trailing)
HStack {
Image(systemName:"bubble.left")
.accentColor(.black)
Text("45")
.foregroundColor(.black)
}
}
}
}
}
}
struct StoryDetails_Previews: PreviewProvider {
static var previews: some View {
StoryDetails(
story: Story(
id: 1,
author: 1,
body: "Testing",
edited_time: "September 2019",
pub_date: "October 2018",
comments: [Comment](),
story_likes: [StoryLike]()
)
)
}
}
Error:
Hi there first I need to see the StoryDetails() but if StoryDetails a Story it should be declared inside as var story: Story let me explain more in example code:
Here you can see my network manager class:
class NetworkManager: ObservableObject {
let url: String = "https://jsonplaceholder.typicode.com/todos/1"
var objectWillChange = PassthroughSubject<NetworkManager, Never>()
init() {
fetchData()
}
#Published var user: User? {
didSet {
objectWillChange.send(self)
print("set user")
}
}
func fetchData() {
guard let url = URL(string: url) else {return}
print("fetch data")
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error == nil else {return}
print("no error")
guard let data = data else {return}
print("data is valid")
let user = try! JSONDecoder().decode(User.self, from: data)
DispatchQueue.main.async {
self.user = user
}
}.resume()
}
}
that's my content view where network manager is initialized inside:
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
VStack {
DetailsView(user: networkManager.user)
}
}
}
Details view struct holds user variable:
struct DetailsView: View {
var user: User?
var body: some View {
VStack {
Text("id: \(user?.id ?? 0)")
Text("UserID: \(user?.userId ?? 0 )")
Text("title: \(user?.title ?? "Empty")")
}
}
}
and that's the DetailsView as you can see inside of this struct I declared a user object of type User need to be pass it so if I want to show it in PreviewProvider it would be like the code below
struct DetailsView_Previews: PreviewProvider {
static var previews: some View {
DetailsView(user: User(id: 0, userId: 0, title: "hello", completed: false)
}
}
model:
struct User: Decodable {
var userId: Int = 0
var id: Int = 0
var title: String = ""
var completed: Bool = false
}
PS: For sure you can unwrap better than this way to provide
any nil exception it's just POC