Pass Object details to another view [SwiftUI] - swiftui

Facing problem to understand how to move Object details to another view using NavigationLink, I have read many articles and watched video, they all do the same as I do except for the Preview struct, they use local data and easily they map the view to the first item of the data like data[0]. While in my case, I fetch the data online, hence the above way did not help me to overcome the issue with the Preview struct, ERROR: Missing argument for parameter
Articles have been read:
developer.apple.com/tutorials/swiftui/building-lists-and-navigation
www.raywenderlich.com/5824937-swiftui-tutorial-navigation
www.youtube.com/watch?v=BCSP_uh0co0&ab_channel=azamsharp
/// Main View Code:
import SwiftUI
import SDWebImageSwiftUI
struct HomeView: View {
#State var posts: [Posts] = []
#State var intPageNo: Int = 0
var body: some View {
NavigationView {
List(posts) {post in
NavigationLink(destination: ViewPostView(post: post)){
VStack{
HStack{
WebImage(url: URL(string: post.featured_image))
.resizable()
.placeholder(Image("logo"))
.frame(width: 150, height: 150, alignment: .center)
VStack(alignment: .leading, spacing: 10.0){
Text("By: \(post.author_name)")
Text("Since: \(post.since)")
Text("City: \(post.city_name)")
Text("Category: \(post.category_name)")
}
.font(.footnote)
Spacer()
}
Text(post.title)
.font(.body)
.fontWeight(.bold)
.frame(alignment: .trailing)
.flipsForRightToLeftLayoutDirection(true)
}
}
}
.onAppear{
self.intPageNo += 1
ApiPosts().getPosts(intPage: self.intPageNo){(posts) in
self.posts = posts
}
}
.navigationBarTitle(Text("Home"))
}
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
/// Detail View Code:
import SwiftUI
struct ViewPostView: View {
#State var comments: [Comments] = []
#State var post: Posts
var body: some View {
NavigationView {
VStack{
Text(post.post_content)
.frame(alignment: .trailing)
List(comments){comment in
VStack(alignment: .trailing, spacing: 10){
HStack(spacing: 40){
Text(comment.author_name)
Text(comment.comment_date)
}
Text(comment.comment_content)
}
}
.frame(alignment: .trailing)
.onAppear {
PostViewManager().getComments(intPostID: self.post.id){(comments) in
self.comments = comments
}
}
}
}
}
}
struct ViewPostView_Previews: PreviewProvider {
static var previews: some View {
ViewPostView()
}
}
/// Fetching data Code:
struct Comments: Codable, Identifiable {
var id: Int
var author_name: String
var comment_content: String
var comment_date: String
var comment_date_gmt: String
var approved: String
}
class PostViewManager {
func getComments(intPostID: Int, completion: #escaping ([Comments]) -> ()){
guard let url = URL(string: "https://test.matjri.com/wp-json/matjri/v1/comments/\(intPostID)") else {return}
URLSession.shared.dataTask(with: url) { (data, _, _) in
let comments = try! JSONDecoder().decode([Comments].self, from: data!)
DispatchQueue.main.async {
completion(comments)
}
}
.resume()
}
}
struct Posts: Codable, Identifiable {
var id: Int
var title: String
var city_id: Int
var city_name: String
var category_id: Int
var category_name: String
var since: String
var author_id: String
var author_name: String
var post_content: String
var featured_image: String
}
class ApiPosts {
func getPosts(intPage: Int, completion: #escaping ([Posts]) -> ()){
guard let url = URL(string: "https://test.matjri.com/wp-json/matjri/v1/posts/0") else {return}
URLSession.shared.dataTask(with: url) { (data, _, _) in
let posts = try! JSONDecoder().decode([Posts].self, from: data!)
DispatchQueue.main.async {
completion(posts)
}
}
.resume()
}
}

The error you get "Preview struct, ERROR: Missing argument for parameter", typically is because you did not provide the required parameters to the Preview.
ViewPostView expect to be passed "var post: Posts", so in ViewPostView_Previews you
need to provide that, for example:
struct ViewPostView_Previews: PreviewProvider {
static var previews: some View {
ViewPostView(post: Posts(id: 1, title: "title", ... ))
}
}

Related

SwiftUI - modifiying variable foreach on View before onTapGesture()

Foreach on view must be presented with a View to process.
struct Home : View {
private var numberOfImages = 3
#State var isPresented : Bool = false
#State var currentImage : String = ""
var body: some View {
VStack {
TabView {
ForEach(1..<numberOfImages+1, id: \.self) { num in
Image("someimage")
.resizable()
.scaledToFill()
.onTapGesture() {
currentImage = "top_00\(num)"
isPresented.toggle()
}
}
}.fullScreenCover(isPresented: $isPresented, content: {FullScreenModalView(imageName: currentImage) } )
}
}
I'm trying to display an image in fullScreenCover. My problem is that the first image is empty. Yes, we can solve this defining at the beginning, however, this will complicate the code according to my experiences.
My question is, is it possible to assign a value to currentImage before the onTapGesture processed.
In short, what is the good practice here.
What you need is to use this modifier to present your full screen modal:
func fullScreenCover<Item, Content>(item: Binding<Item?>, onDismiss: (() -> Void)? = nil, content: #escaping (Item) -> Content) -> some View where Item : Identifiable, Content : View
You pass in a binding to an optional and uses the non optional value to construct a destination:
struct ContentView: View {
private let imageNames = ["globe.americas.fill", "globe.europe.africa.fill", "globe.asia.australia.fill"]
#State var selectedImage: String?
var body: some View {
VStack {
TabView {
ForEach(imageNames, id: \.self) { imageName in
Image(systemName: imageName)
.resizable()
.scaledToFit()
.padding()
.onTapGesture() {
selectedImage = imageName
}
}
}
.tabViewStyle(.page(indexDisplayMode: .automatic))
.fullScreenCover(item: $selectedImage) { imageName in
Destination(imageName: imageName)
}
}
}
}
struct Destination: View {
let imageName: String
var body: some View {
ZStack {
Color.blue
Image(
systemName: imageName
)
.resizable()
.scaledToFit()
.foregroundColor(.green)
}
.edgesIgnoringSafeArea(.all)
}
}
You will have to make String identifiable for this example to work (not recommended):
extension String: Identifiable {
public var id: String { self }
}
Building upon #LuLuGaGa’s answer (accept that answer, not this one), instead of making String Identifiable, create a new Identifiable struct to hold the image info. This guarantees each image will have a unique identity, even if they use the same base image name. Also, the ForEach loop now becomes ForEach(imageInfos) since the array contains Identifiable objects.
Use map to turn image name strings into [ImageInfo] by calling the ImageInfo initializer with each name.
This example also puts the displayed image into its own view which can be dismissed with a tap.
import SwiftUI
struct ImageInfo: Identifiable {
let name: String
let id = UUID()
}
struct ContentView: View {
private let imageInfos = [
"globe.americas.fill",
"globe.europe.africa.fill",
"globe.asia.australia.fill"
].map(ImageInfo.init)
#State var selectedImage: ImageInfo?
var body: some View {
VStack {
TabView {
ForEach(imageInfos) { imageInfo in
Image(systemName: imageInfo.name)
.resizable()
.scaledToFit()
.padding()
.onTapGesture() {
selectedImage = imageInfo
}
}
}
.tabViewStyle(.page(indexDisplayMode: .automatic))
.fullScreenCover(item: $selectedImage) { imageInfo in
ImageDisplay(info: imageInfo)
}
}
}
}
struct ImageDisplay: View {
let info: ImageInfo
#Environment(\.dismiss) var dismiss
var body: some View {
ZStack {
Color.blue
Image(
systemName: info.name
)
.resizable()
.scaledToFit()
.foregroundColor(.green)
}
.edgesIgnoringSafeArea(.all)
.onTapGesture {
dismiss()
}
}
}

Swiftui Mapping Nested JSON to Flat Model

I need to access the data in the nested dictionary of the Memodel struct. From both the music and image dictionary. Please any help is needed to map out correctly, i have tried using AzampSharp's example https://www.youtube.com/watch?v=b5wVIQNrI6k but i believe i am doing something wrong. Thanks.
import SwiftUI
struct MemodelAPIResult: Codable {
let data: [Memodel]
enum CodingKeys: String, CodingKey {
case data = "results"
}
}
struct Memodel: Identifiable, Codable {
var id: String
var followers: String
var following: String
let music: [MemodelMusic]
let images: [MemodelImages]
}
struct MemodelMusic: Identifiable, Codable, Hashable {
var id: String
var musicfile: URL
var musicname: String
var musicartistname: String
var musicgenre: String
}
struct MemodelImages: Identifiable, Codable, Hashable {
var id: String
var albumimages: URL
var abumlikes: String
var albumviews: String
}
Below is my ObservableObject in my View Model
import Foundation
import SwiftUI
import Combine
import CryptoKit
class MeViewmodel: ObservableObject {
#Published var me: [Memodel]? = nil
init() {
self.fetchme()
}
func fetchme() {
let url = ""
let session = URLSession(configuration: .default)
session.dataTask(with: URL(string: url)!) { (data, _, err) in
if let error = err{
print(error.localizedDescription)
return
}
guard let APIData = data else {
print("No Data found")
return
}
do {
let new = try JSONDecoder().decode(MemodelAPIResult.self, from: APIData)
DispatchQueue.main.async {
self.me = new.data
}
}
catch{
print(error)
}
}
.resume()
}
}
And then the item view
struct MeMusicItemView: View {
//E-MARK: - Properties
var me: Memodel
//E-MARK: - Body
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 5) {
Text(me.music[0].musicname)
.font(.callout)
.fontWeight(.medium)
.foregroundColor(.white)
Text(me.music[0].musicartistname)
.font(.caption2)
.fontWeight(.light)
.foregroundColor(.white)
Text(me.music[0].musicgenre)
.font(.system(size: 8))
.fontWeight(.light)
.foregroundColor(.gray)
}
}
}
}
And also the ForEach in the parent View....
if let meMusicData = meMusicData.mememe {
ForEach(meMusicData) { music in
MeMusicItemView(memusic: music)
}
} else {
ProgressView()
.padding(.top, 20)
}
There is not enough info for me to really understand what you are doing, but
here is some code you can have a look at and recycle for your purpose:
struct ContentView: View {
#StateObject var viewModel = MeViewmodel() // <-- here your model
var body: some View {
List {
ForEach(viewModel.me) { memod in // <-- loop over the Memodel array
ForEach(memod.music) { music in // <-- loop over the MemodelMusic array
MeMusicItemView(memusic: music) // <-- display 1 MemodelMusic
}
}
}
}
}
struct MeMusicItemView: View {
//E-MARK: - Properties
#State var memusic: MemodelMusic // <-- here
//E-MARK: - Body
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 5) {
Text(memusic.musicname)
.font(.callout)
.fontWeight(.medium)
.foregroundColor(.pink)
Text(memusic.musicartistname)
.font(.caption2)
.fontWeight(.light)
.foregroundColor(.green)
Text(memusic.musicgenre)
.font(.system(size: 8))
.fontWeight(.light)
.foregroundColor(.blue)
}
}
}
}
class MeViewmodel: ObservableObject {
#Published var me: [Memodel] = [] // <--- here no optional, it is easier to deal with
init() {
self.fetchme()
}
func fetchme() {
// ......
}
}
struct Memodel: Identifiable, Codable {
var id: String
var followers: String
var following: String
let music: [MemodelMusic]
let images: [MemodelImages]
}
struct MemodelMusic: Identifiable, Codable, Hashable {
var id: String
var musicfile: URL
var musicname: String
var musicartistname: String
var musicgenre: String
}
struct MemodelImages: Identifiable, Codable, Hashable {
var id: String
var albumimages: URL
var abumlikes: String
var albumviews: String
}

Cannot get view to update with downloaded JSON

I am stuck at the simplest place right now.. I'm making a network request and just want the view to be updated once the JSON is received..
And I verified that:
JSON is valid
Valid response received (verified in print statement)
I've done this about 50 times and don't know why I'm stuck at this point.
struct ContentView: View {
#StateObject var nm = NetworkManager()
var body: some View {
VStack {
ScrollView {
HStack {
ForEach(nm.articles, id: \.hashValue) { article in
Text("Hello")
}
}.task {
do {
try await NetworkManager().getAllArticles(for: "mario")
} catch { print(error) }
}
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
final class NetworkManager: ObservableObject {
#Published var newsItem: News?
#Published var articles: [Article] = []
private let apiKey = ""//removed
private var baseUrlString: String {
"https://newsapi.org/v2/"
}
func getAllArticles(for searchItem: String) async throws {
let url = URL(string: baseUrlString + "everything?q=\(searchItem)&apiKey=\(apiKey)")!
let (data, _) = try await URLSession.shared.data(from: url)
let news = try JSONDecoder().decode(News.self, from: data)
DispatchQueue.main.async {
self.newsItem = news
self.articles = self.newsItem!.articles
}
}
}
struct News: Codable {
var totalResults: Int?
let articles: [Article]
}
struct Article: Codable, Hashable {
let author: String?
let title: String
let description: String
let url: String
let urlToImage: String?
let publishedAt: String
let content: String
}
Edit: removed apiKey
getAllArticles() is view-related, which means you should probably implement this function inside the View instead of ObservableObject.
struct ContentView: View {
#StateObject var nm = NetworkManager()
var body: some View {
VStack {
ScrollView {
VStack {
ForEach(nm.articles, id: \.hashValue) { article in
Text("Hello")
}
}.task {
do {
try await getAllArticles(for: "mario")
} catch { print(error) }
}
}
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
extension ContentView {
func getAllArticles(for searchItem: String) async throws {
let url = URL(string: nm.baseUrlString + "everything?q=\(searchItem)&apiKey=\(NetworkManager.apiKey)")!
let (data, _) = try await URLSession.shared.data(from: url)
let news = try JSONDecoder().decode(News.self, from: data)
// Not necessary
// DispatchQueue.main.async {
nm.newsItem = news
nm.articles = nm.newsItem!.articles
// }
}
}
final class NetworkManager: ObservableObject {
#Published var newsItem: News?
#Published var articles: [Article] = []
static let apiKey = "YOUR_API_KEY"
var baseUrlString: String {
"https://newsapi.org/v2/"
}
}

Display filename in next View too

I have a code that makes a http Request, gets an array with filenames from that, displays them each with an image and the filename below. Everything works fine.
Now I made each image a button that opens a detail page.
That works but at the top it should say the matching filename from the page before.
But I am not able to hand over the filename (name) from ContentView4 to the next page (ts).
The language is SwiftUi
Could you please help me?
Thanks
Nikias
Here is my code:
import SwiftUI
struct ContentView4: View {
#State var showingDetail = false
#State var username: String = "."
#State var password: String = "."
#State private var name = String("Nikias2")
#State private var t = String()
#State private var x = -1
#State var dateien = ["word.png"]
var body: some View {
ScrollView(.vertical) {
ZStack{
VStack {
ForEach(0 ..< dateien.count, id: \.self) {
Button(action: {
print("button pressed")
x = x + 1
t = dateien[x]
self.showingDetail.toggle()
}) {
Image("datei")
}
.scaledToFit()
.padding(0)
Text(self.dateien[$0])
Text(t)
.foregroundColor(.white)
}
}
}
.sheet(isPresented:
$showingDetail) {
ts(name: t)
}
.onAppear { //# This `onAppear` is added to `ZStack{...}`
doHttpRequest()
}
}
}
func doHttpRequest() {
let myUrl = URL(string: "http://192.168.1.180/int.php")! //# Trailing semicolon is not needed
var request = URLRequest(url: myUrl)
request.httpMethod = "POST"// Compose a query string
let postString = "Name=\($username)&Passwort=\($password)"
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) {
(data, response, error) in
//# Use if-let when you want to use the unwrapped value
if let error = error {
print("error=\(error)")
return
}
//# Use guard-let when nil has no meaning and want to exit on nil
guard let response = response else {
print("Unexpected nil response")
return
}
// You can print out response object
print("response = \(response)")
//Let's convert response sent from a server side script to a NSDictionary object:
do {
//# Use guard-let when nil has no meaning and want to exit on nil
guard let data = data else {
print("Unexpected nil data")
return
}
//#1 `mutableContainer` has no meaning in Swift
//#2 Use Swift Dictionary type instead of `NSDictionary`
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
if let parseJSON = json {
// Now we can access value of First Name by its key
//# Use if-let when you want to use the unwrapped value
if let firstNameValue = parseJSON["Name"] as? String {
print("firstNameValue: \(firstNameValue)")
let dateien = firstNameValue.components(separatedBy: ",")
print(dateien)
self.dateien = dateien
}
}
} catch {
print(error)
}
}
task.resume()
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
ContentView4()
}
}
struct ts: View {
#State var hin = false
#State var um = false
#State var datname: String = ""
var name: String
var body: some View {
NavigationView {
VStack {
Text(name)
.font(.system(size: 60))
.foregroundColor(.black)
.padding(50)
Button(action: {
self.hin.toggle()
}) {
Text("+")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.yellow)
.cornerRadius(35.0)
}
.padding()
if hin {
HStack {
Text("Datei auswählen")
.font(.headline)
.frame(width: 150, height: 70)
.background(Color.yellow)
.cornerRadius(20.0)
.animation(Animation.default)
Text("Datei hochladen")
.font(.headline)
.frame(width: 150, height: 70)
.background(Color.yellow)
.cornerRadius(20.0)
.animation(Animation.default)
}
}
Text("Datei herunterladen")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.blue)
.cornerRadius(35.0)
Button(action: {
self.um.toggle()
}) {
Text("Datei umbenennen")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.green)
.cornerRadius(35.0)
}
.padding()
if um {
HStack {
TextField(name, text: $datname)
.font(.headline)
.frame(width: 150, height: 70)
.cornerRadius(20.0)
.animation(Animation.default)
Text("Datei umbenennen")
.font(.headline)
.frame(width: 150, height: 70)
.background(Color.green)
.cornerRadius(20.0)
.animation(Animation.default)
}
}
Text("Datei löschen")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.red)
.cornerRadius(35.0)
}
}
}
}
I believe your issue is a result of using #State variables to store all of the attributes. #State variables are not consistent and get refreshed in the background by SwiftUI depending on your views visibility.
The piece that you are missing is a view controller class stored in an #EnviornmentObject variable. This class gets Initiated in your main contentView and is used to keep track and alter of all your attributes.
Each ContentView should reference the single #EnviornmentObject and pull data from that class.
Another solution which may work would be to replace all your #State variables with #StateObject vars. #StateObject vars are basically #State vars but get initiated before the struct get loaded and the value is kept consistent regardless of the view state of the parent struct.
Here is a rough implementation of #EnvironmentObject within your project.
Basically use the #EnvironmentObject to pass values to child views
ContentView4.swift
struct ContentView4: View {
#EnvironmentObject cv4Controller: ContentView4Controller
var body: some View {
ScrollView(.vertical) {
ZStack{
VStack {
ForEach(0 ..< cv4Controller.dateien.count, id: \.self) {
Button(action: {
print("button pressed")
x = x + 1
t = cv4Controller.dateien[x]
self.showingDetail.toggle()
}) {
Image("datei")
}
.scaledToFit()
.padding(0)
Text(self.dateien[$0])
Text(cv4Controller.t)
.foregroundColor(.white)
}
}
}
.sheet(isPresented:
cv4Controller.$showingDetail) {
ts(name: cv4Controller.t)
}
.onAppear { //# This `onAppear` is added to `ZStack{...}`
cv4Controller.doHttpRequest()
}
}
}
ContentView4Controller.swift
class ContentView4Controller: ObservableObject {
#Published var showingDetail = false
#Published var username: String = "."
#Published var password: String = "."
#Published private var name = String("Nikias2")
#Published private var t = String()
#Published private var x = -1
#Published private var t = String()
#Published private var x = -1
#Published var dateien = ["word.png"]
func doHttpRequest() {
let myUrl = URL(string: "http://192.168.1.180/int.php")! //# Trailing semicolon is not needed
var request = URLRequest(url: myUrl)
request.httpMethod = "POST"// Compose a query string
let postString = "Name=\($username)&Passwort=\($password)"
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) {
(data, response, error) in
//# Use if-let when you want to use the unwrapped value
if let error = error {
print("error=\(error)")
return
}
//# Use guard-let when nil has no meaning and want to exit on nil
guard let response = response else {
print("Unexpected nil response")
return
}
// You can print out response object
print("response = \(response)")
//Let's convert response sent from a server side script to a NSDictionary object:
do {
//# Use guard-let when nil has no meaning and want to exit on nil
guard let data = data else {
print("Unexpected nil data")
return
}
//#1 `mutableContainer` has no meaning in Swift
//#2 Use Swift Dictionary type instead of `NSDictionary`
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
if let parseJSON = json {
// Now we can access value of First Name by its key
//# Use if-let when you want to use the unwrapped value
if let firstNameValue = parseJSON["Name"] as? String {
print("firstNameValue: \(firstNameValue)")
let dateien = firstNameValue.components(separatedBy: ",")
print(dateien)
self.dateien = dateien
}
}
} catch {
print(error)
}
}
task.resume()
}
}
Example of main ContentView.swift
struct ContentView: View {
var cv4Controller: ContentView4Controller = ContentView4Controller()
var body: some view {
// your main page output
GeometryReader { geo in
// just a guess for what you have in your main contentView
switch(page) {
case .main:
ContentView2()
default:
ContentView4()
break
}
}.environmentObject(cv4Controller) // this will make cv4Controller available to all child view structs
}
}
Add #Binding wrapper to the "name" variable in your ts view. And pass the t variable as a binding by adding a "$". This will keep your ts name variable updated to whatever is value it has in the parent view.
Also why do you use a NavigationView in your ts View?
struct ContentView4: View {
...
#State private var t = String()
...
var body: some View {
...
ZStack{
...
}
.sheet(isPresented: $showingDetail) {
ts(name: $t)
}
...
}
func doHttpRequest() {
...
}
}
struct ts: View {
...
#Binding var name: String
var body: some View {
...
}
}
My starting code works, but It's just displaying the Filenames in a row and if I tap a random image, the name won't fit, only if I'm going down in the row and tap them. The problem is, that I don't know how to set the variable to the id, not to pass them to the next view. Has anyone got and idea how I can pass the right filename into a variable in the for loop and read it in the next view?

SwiftUI http request

I need to post a http request to login view (SwiftUI), my code follow
I have in HttpAuth.swift:
import Foundation
import Combine
struct ServerMessage: Decodable {
let res, message: String
}
class HttpAuth: ObservableObject {
let objectWillChange = PassthroughSubject<HttpAuth, Never>()
var authenticated = false {
didSet {
objectWillChange.send(self)
}
}
func postAuth(username: String, password: String) {
guard let url = URL(string: "http://mysite/loginswift.php") else { return }
let body: [String: String] = ["username": username, "password": password]
let finalBody = try! JSONSerialization.data(withJSONObject: body)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = finalBody
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else { return }
let resData = try! JSONDecoder().decode(ServerMessage.self, from: data)
print(resData.res)
if resData.res == "correct" {
DispatchQueue.main.async {
self.authenticated = true
}
}
}.resume()
}
}
And in ContentView.swift:
import SwiftUI
struct ContentView: View {
#State private var username: String = ""
#State private var password: String = ""
#State var manager = HttpAuth()
var body: some View {
VStack(alignment: .leading) {
if self.manager.authenticated {
Text("Correct!").font(.headline)
}
Text("Username")
TextField("placeholder", text: $username)
.textFieldStyle(RoundedBorderTextFieldStyle())
.border(Color.green)
.autocapitalization(.none)
Text("Password")
SecureField("placeholder", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.border(Color.green)
Button(action: {
self.manager.postAuth(username: self.username, password: self.password)
}) {
HStack{
Spacer()
Text("Login")
Spacer()
}
.accentColor(Color.white)
.padding(.vertical, 10)
.background(Color.red)
.cornerRadius(5)
.padding(.horizontal, 40)
}
}.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
It works, i receive the answer from server but it doesn't update the ContentView, the 'self.manager.authenticated' in ContentView doesn't refresh from HttpAuth class.
This part of code doesn't work:
if self.manager.authenticated {
Text("Correct!").font(.headline)
}
The 'authenticated' still on false.
How can i fix it, thank you.
Although the accepted answer meets the objective I don't think it's the best way to do it because it implements #EnvironmentObject and it should be used for global application issues and not for the specific request of a view.
You can implement a "ViewModel" that is specific for the view, to do the work of the request and make the update of the variable.
Documentation: https://developer.apple.com/documentation/combine/observableobject
The implementation for your code would be like this, based on the changes suggested by #Chris:
This works in Xcode Version 11.2 (11B52)
HttpAuth.swif:
import Foundation
import SwiftUI
import Combine
class HttpAuth: ObservableObject {
#Published var authenticated = false
func postAuth(username: String, password: String) {
guard let url = URL(string: "http://mysite/loginswift.php") else { return }
let body: [String: String] = ["username": username, "password": password]
let finalBody = try! JSONSerialization.data(withJSONObject: body)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = finalBody
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else { return }
let resData = try! JSONDecoder().decode(ServerMessage.self, from: data)
print(resData.res)
if resData.res == "correct" {
DispatchQueue.main.async {
self.authenticated = true
}
}
}.resume()
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#State private var username: String = ""
#State private var password: String = ""
#ObservedObject var manager = HttpAuth()
var body: some View {
VStack(alignment: .leading) {
if self.manager.authenticated {
Text("Correct!").font(.headline)
}
Spacer()
Text("Username")
TextField("placeholder", text: $username)
.textFieldStyle(RoundedBorderTextFieldStyle())
.border(Color.green)
.autocapitalization(.none)
Text("Password")
SecureField("placeholder", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.border(Color.green)
Button(action: {
self.manager.postAuth(username: self.username, password: self.password)
}) {
HStack{
Spacer()
Text("Login")
Spacer()
}
.accentColor(Color.white)
.padding(.vertical, 10)
.background(Color.red)
.cornerRadius(5)
.padding(.horizontal, 40)
}
Spacer()
}.padding()
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
you can try this:
instead of
let objectWillChange = PassthroughSubject<HttpAuth, Never>()
var authenticated = false {
didSet {
objectWillChange.send(self)
}
}
use
#published var authenticated = false
and instead of
#State var manager = HttpAuth()
use
#EnvironmentObject private var manager: HttpAuth
and of course do this when calling ContentView:
ContentView().environmentObject(manager)
and somewhere outside class as global variable do
var manager = HttpAuth()
then it should work.