How to initialize data in Preview provider in SwiftUI - swift

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

Related

SwiftUI: #State value doesn't update after async network request

My aim is the change data in DetailView(). Normally in this case I'm using #State + #Binding and it's works fine with static data, but when I trying to update ViewModel with data from network request I'm loosing functionality of #State (new data doesn't passing to #State value it's stays empty). I checked network request and decoding process - everything ok with it. Sorry my code example a bit long but it's the shortest way that I found to recreate the problem...
Models:
struct LeagueResponse: Decodable {
var status: Bool?
var data: [League] = []
}
struct League: Codable, Identifiable {
let id: String
let name: String
var seasons: [Season]?
}
struct SeasonResponse: Codable {
var status: Bool?
var data: LeagueData?
}
struct LeagueData: Codable {
let name: String?
let desc: String
let abbreviation: String?
let seasons: [Season]
}
struct Season: Codable {
let year: Int
let displayName: String
}
ViewModel:
class LeagueViewModel: ObservableObject {
#Published var leagues: [League] = []
init() {
Task {
try await getLeagueData()
}
}
private func getLeagueData() async throws {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api-football-standings.azharimm.site/leagues")!)
guard let leagues = try? JSONDecoder().decode(LeagueResponse.self, from: data) else {
throw URLError(.cannotParseResponse)
}
await MainActor.run {
self.leagues = leagues.data
}
}
func loadSeasons(forLeague id: String) async throws {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api-football-standings.azharimm.site/leagues/\(id)/seasons")!)
guard let seasons = try? JSONDecoder().decode(SeasonResponse.self, from: data) else {
throw URLError(.cannotParseResponse)
}
await MainActor.run {
if let responsedLeagueIndex = leagues.firstIndex(where: { $0.id == id }),
let unwrappedSeasons = seasons.data?.seasons {
leagues[responsedLeagueIndex].seasons = unwrappedSeasons
print(unwrappedSeasons) // successfully getting and parsing data
}
}
}
}
Views:
struct ContentView: View {
#StateObject var vm = LeagueViewModel()
var body: some View {
NavigationView {
VStack {
if vm.leagues.isEmpty {
ProgressView()
} else {
List {
ForEach(vm.leagues) { league in
NavigationLink(destination: DetailView(league: league)) {
Text(league.name)
}
}
}
}
}
.navigationBarTitle(Text("Leagues"), displayMode: .large)
}
.environmentObject(vm)
}
}
struct DetailView: View {
#EnvironmentObject var vm: LeagueViewModel
#State var league: League
var body: some View {
VStack {
if let unwrappedSeasons = league.seasons {
List {
ForEach(unwrappedSeasons, id: \.year) { season in
Text(season.displayName)
}
}
} else {
ProgressView()
}
}
.onAppear {
Task {
try await vm.loadSeasons(forLeague: league.id)
}
}
.navigationBarTitle(Text("League Detail"), displayMode: .inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
ChangeButton(selectedLeague: $league)
}
}
}
}
struct ChangeButton: View {
#EnvironmentObject var vm: LeagueViewModel
#Binding var selectedLeague: League // if remove #State the data will pass fine
var body: some View {
Menu {
ForEach(vm.leagues) { league in
Button {
self.selectedLeague = league
} label: {
Text(league.name)
}
}
} label: {
Image(systemName: "calendar")
}
}
}
Main goals:
Show selected league seasons data in DetailView()
Possibility to change seasons data in DetailView() when another league was chosen in ChangeButton()
You update view model but DetailView contains a copy of league (because it is value type).
The simplest seems to me is to return in callback seasons, so there is possibility to update local league as well.
func loadSeasons(forLeague id: String, completion: (([Season]) -> Void)?) async throws {
// ...
await MainActor.run {
if let responsedLeagueIndex = leagues.firstIndex(where: { $0.id == id }),
let unwrappedSeasons = seasons.data?.seasons {
leagues[responsedLeagueIndex].seasons = unwrappedSeasons
completion?(unwrappedSeasons) // << here !!
}
}
}
and make task dependent on league id so selection would work, like:
struct DetailView: View {
#EnvironmentObject var vm: LeagueViewModel
#State var league: League
var body: some View {
VStack {
// ...
}
.task(id: league.id) { // << here !!
Task {
try await vm.loadSeasons(forLeague: league.id) {
league.seasons = $0 // << update local copy !!
}
}
}
Tested with Xcode 13.4 / iOS 15.5
Test module is here
One question is if you already made LeagueViewModel an ObservableObject, why don't you display from it directly, and simply pass an id to your DetailView?
So your detail view will be:
struct DetailView: View {
#EnvironmentObject var vm: LeagueViewModel
#State var id: String
var body: some View {
VStack {
if let unwrappedSeasons = vm.leagues.first { $0.id == id }?.seasons {
List {
ForEach(unwrappedSeasons, id: \.year) { season in
Text(season.displayName)
}
}
} else {
ProgressView()
}
}
.task(id: id) {
Task {
try await vm.loadSeasons(forLeague: id)
}
}
.navigationBarTitle(Text("League Detail"), displayMode: .inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
ChangeButton(selectedId: $id)
}
}
}
}
The view will automatically update season data as it loads them.

Read json-data from url, but it would not viewed

I have a api that give me the follong response:
[{
"id": 1,
"title": "Learn Grinding"
}, {
"id": 2,
"title": "See the messure".
}]
and in my view I like to view this in a list. Later I like to save it into Core Data. But for testing, i just like to see the values in a list. But there is nothing to see :(
The list is empty. With the same content I try to use the Apple-Example with the itunes-List and this work in this ViewController.
With the first Button I test, if I can add a value to the database, and this works fine. Only the List of the json-data would not be read and viewed.
import SwiftUI
struct Response: Codable {
var results: [Result]
}
struct Result: Codable {
var id: Int
var title: String
}
struct TutorialsView: View {
#StateObject var viewRouter: ViewRouter
#State private var results = [Result]()
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: Tutorials.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Tutorials.title, ascending: true)]
) var tutorials: FetchedResults<Tutorials>
var body: some View {
VStack {
Text("Tutorials")
.bold()
Button(action: {
let tutorial = Tutorials(context: managedObjectContext)
tutorial.title = "Describe how to messure"
PersistenceController.shared.save()
}, label: {
Text("Add Tutorial")
})
List {
ForEach(tutorials, id:\.self) { tutorial in
Text("\(tutorial.title ?? "Unknown")")
}
}
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
}
}
.task {
await loadData()
}
}
}
func loadData() async {
guard let url = URL(string: "https://example.org") else {
print("Invalid URL")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
results = decodedResponse.results
}
} catch {
print("Invalid data")
}
}
}
struct TutorialsView_Previews: PreviewProvider {
static var previews: some View {
TutorialsView(viewRouter: ViewRouter())
}
}
When I change the url from the itunes to my own, I got this error in the console:
2022-02-17 14:36:19.868821+0100 GrindingCalculator[2460:27599] [boringssl]
boringssl_metrics_log_metric_block_invoke(151) Failed to log metrics
I also try to use a iPhone and install it on this device and there also would be nothing viewed.
Can you see, where my mistake is?

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()
}
}

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

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