Can't figure out swift POST request error - swift

I'm trying to make a login screen in SwiftUI that calls a server via a POST request but I'm getting an unknown error (I have a print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")") ).
The server gets the request and the data in the POST request and returns: {"correctCredentials": true, "message": ""} but the json decoder is not working and I don't know why, could someone take a look at my code?
import SwiftUI
struct serverResponse: Codable {
var loginResults: [loginResult]
}
struct loginResult: Codable {
var correctCredentials: Bool
var message: String
}
struct credentialsFormat: Codable {
var username: String
var password: String
}
struct loginView: View {
func getData() {
guard let url = URL(string: "https://example.com/api/login") else {
print("Invalid URL")
return
}
guard let encoded = try? JSONEncoder().encode(credentialsFormat(username: username, password: password)) else {
print("Failed to encode data")
return()
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = encoded
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
print("here")
let status = (response as! HTTPURLResponse).statusCode
print(status)
//if (status == 200) {
if let decodedResponse = try? JSONDecoder().decode(serverResponse.self, from: data) {
// we have good data – go back to the main thread
print("here1")
DispatchQueue.main.async {
// update our UI
self.results = decodedResponse.loginResults
}
// everything is good, so we can exit
return
}
//} else { print("Status is not 200"); return } //from if status == 200
}
// if we're still here it means there was a problem
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
#State private var results = [loginResult]()
#State private var username: String = ""
#State private var password: String = ""
#State private var showingAlert: Bool = false
var body: some View {
VStack{
TextField("Username", text: $username)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 200, height: nil)
.multilineTextAlignment(.center)
.disableAutocorrection(Bool(true))
.accessibility(identifier: "Username")
.autocapitalization(.none)
SecureField("Password", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 200, height: nil)
.multilineTextAlignment(.center)
.disableAutocorrection(true)
.accessibility(identifier: "Password")
Button(action: {
self.getData()
//if self.results.correctCredentials {
//self.showingAlert = true
//}
//print(self.username + ", " + self.password)
print(self.results)
}) {
HStack{
Spacer()
Text("Login").font(.headline).foregroundColor(.white)
Spacer()
}.padding(.vertical, CGFloat(10))
.background(Color.red)
.padding(.horizontal, CGFloat(40))
}
.alert(isPresented: $showingAlert) {
Alert(title: Text("Wrong Credentials"), message: Text("The username and/or password that you entered is wrong"), dismissButton: .default(Text("Got it!")))
}
}
}
}
btw, I am quite new to swift and got the URLSession for a GET request from hacking with swift and tried to convert it to POST

It looks like your server is returning loginResult instead of serverResponse. Could you try
if let decodedResponse = try? JSONDecoder().decode(loginResult.self, from: data) {
print(decodedResponse)
DispatchQueue.main.async {
self.results = [decodedResponse]
}
return
}

Related

Updating specific list item after patch request Swift, SwiftUI

I'm using GitHub api to
get issues
update issue title or description
I call self.get() inside update() method, to get the updated list of issues after the successful update.
But after update when I get back to screen with list of issues, its title/description shows the previous values.
Could you please say how can I fix it? Thanks!
Here is the model:
struct Issue: Codable, Hashable {
let id: Int
let number: Int
var title: String
var body: String? = ""
}
ViewModel:
class ViewModel: ObservableObject {
#Published var issues: [Issue] = []
private let token: String = "_token_"
init() {
get()
}
func get() {
guard let url = URL(string: "https://api.github.com/repos/\(repoOwner)/\(repo)/issues") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
guard let data = data, error == nil else {
return
}
do {
let response = try JSONDecoder().decode([Issue].self, from: data)
DispatchQueue.main.async {
self?.issues = response
}
} catch {
print("error during get issues \(error.localizedDescription)")
}
}.resume()
}
func update(number: Int, title: String, body: String) {
guard let url = URL(string: "https://api.github.com/repos/\(repoOwner)/\(repo)/issues/\(number)") else {
return
}
let json: [String: Any] = ["title": "\(title)",
"body": "\(body)"]
let jsonData = try? JSONSerialization.data(withJSONObject: json)
var patchRequest = URLRequest(url: url)
patchRequest.httpMethod = "PATCH"
patchRequest.addValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
patchRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
patchRequest.httpBody = jsonData
URLSession.shared.dataTask(with: patchRequest) { data, response, error in
guard let data = data, error == nil else {
return
}
let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
if let responseJSON = responseJSON as? [String: Any] {
print(responseJSON)
}
self.get()
}.resume()
}
}
Views
List of issues:
struct HomeView: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
NavigationView {
List {
ForEach(viewModel.issues, id: \.id) { issue in
NavigationLink(destination: EditView(issue: issue)) {
IssueRow(issue: issue)
}
}
}
.navigationTitle("Github issues")
}
}
}
struct IssueRow: View {
var issue: Issue
var body: some View {
VStack {
HStack {
Text("#\(issue.number)")
.bold()
Text("\(issue.title)")
.bold()
.lineLimit(1)
Spacer()
}.foregroundColor(.black)
HStack {
Text(issue.body ?? "no description")
.foregroundColor(.gray)
.lineLimit(2)
Spacer()
}
Spacer()
}
}
}
Edit view:
struct EditView: View {
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var viewModel: ViewModel
var issue: Issue
#State var title: String = ""
#State var description: String = ""
func save() {
viewModel.update(number: issue.number, title: title, body: description)
dismiss()
}
func dismiss() {
presentationMode.wrappedValue.dismiss()
}
var body: some View {
Content(issue: issue, title: $title, description: $description, save: save)
}
}
extension EditView {
struct Content: View {
var issue: Issue
#Binding var title: String
#Binding var description: String
let save: () -> Void
var body: some View {
VStack {
List {
Section(header: Text("Title")) {
TextEditor(text: $title)
}
Section(header: Text("Description")) {
TextEditor(text: $description)
.multilineTextAlignment(.leading)
}
}
saveButton
}
.onAppear(perform: setTitleAndDescription)
.navigationTitle("Update issue")
}
var saveButton: some View {
Button(action: save) {
Text("Save changes")
}
}
func setTitleAndDescription() {
self.title = issue.title
self.description = issue.body ?? ""
}
}
}

How could you access the GitHub API in Swift?

I'd like to make an update detection system in my macOS SwiftUI app by pulling the latest release from GitHub via the API and then comparing the tag. How would I go about accessing the API from Swift? I've tried using the methods from here, medium.com, here, swifttom.com and here, steveclarkapps.com but none of them accomplish what I'm trying to do.
For the first method, the code functions with the provided example API, but doesn't work with the GitHub API and it returns this error instead:
Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil))
Method 2 suffers the same issue.
I couldn't even get enough of method 3's code working to try it.
Here's my adapted code based off of the medium.com method:
Model.swift
import Foundation
struct TaskEntry: Codable {
let id: Int
let tag_name: String
let name: String
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var results = [TaskEntry]()
var body: some View {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.name)
}
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://api.github.com/repos/NCX-Programming/RNGTool/releases/latest") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
/*if*/ let response = try! JSONDecoder().decode([TaskEntry].self, from: data) /*{*/
DispatchQueue.main.async {
self.results = response
}
return
/*}*/
}
}.resume()
}
}
Commented out code and variable names that seem irrelevant are just leftovers.
OS: macOS Big Sur 11.6
Xcode version: 13.0
Open this in your browser:
https://api.github.com/repos/NCX-Programming/RNGTool/releases/latest
You will notice it is not an array but an object. You should be decoding an object like this:
JSONDecoder().decode(TaskEntry.self, from: data)
Edit:
This requires you to change your view. Notice this is no longer a List because you are no longer fetching an array but a single item:
struct TaskEntry: Codable {
let id: Int
let tagName: String
let name: String
}
struct ContentView: View {
#State var entry: TaskEntry? = nil
var body: some View {
VStack(alignment: .leading) {
if let entry = entry {
Text("\(entry.id)")
Text(entry.name)
Text(entry.tagName)
} else {
ProgressView()
}
}
.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://api.github.com/repos/NCX-Programming/RNGTool/releases/latest") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
// TODO: Handle data task error
return
}
guard let data = data else {
// TODO: Handle this
return
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let response = try decoder.decode(TaskEntry.self, from: data)
DispatchQueue.main.async {
self.entry = response
}
} catch {
// TODO: Handle decoding error
print(error)
}
}.resume()
}
}
NOTICE: I did some other improvements as well
Use JSONDecoder to convert from snake case to camel case
Added do catch block so your app doesn't crash
Check for errors before decoding
Added loading indicator (had to put something in the else)
However,
As our discussion you are probably calling the wrong endpoint. That endpoint is not returning an array but a single object, you can tell this because the JSON response begins with { rather than [
I've adjusted my answer to change the endpoint I believe you should be calling:
struct TaskEntry: Codable {
let id: Int
let tagName: String
let name: String
}
struct ContentView: View {
#State var results: [TaskEntry]? = nil
var body: some View {
if let results = results {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.name)
}
}
} else {
VStack(alignment: .leading) {
ProgressView()
.onAppear(perform: loadData)
}
}
}
func loadData() {
guard let url = URL(string: "https://api.github.com/repos/NCX-Programming/RNGTool/releases") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
// TODO: Handle data task error
return
}
guard let data = data else {
// TODO: Handle this
return
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let response = try decoder.decode([TaskEntry].self, from: data)
DispatchQueue.main.async {
self.results = response
}
} catch {
// TODO: Handle decoding error
print(error)
}
}.resume()
}
}

SwiftUI functions execute in wrong order

I'm building a SwiftUI app with CRUD functionality with a mysql db. The CRUD operations work fine and after the create, update, or delete function the read function is called to get the updated data from the db. When I run the app the read function seems to be called before the create, update of delete function if I look at the console output so I only see the mutation after running the app again. Any idea how to fix this?
This is my create code
struct NewPostView: View {
#EnvironmentObject var viewModel: ViewModel
#Binding var isPresented: Bool
#Binding var title: String
#Binding var post: String
#State var isAlert = false
var body: some View {
NavigationView {
ZStack {
Color.gray.opacity(0.1).edgesIgnoringSafeArea(.all)
VStack(alignment: .leading) {
Text("Create new post")
.font(Font.system(size: 16, weight: .bold))
TextField("Title", text: $title)
.padding()
.background(Color.white)
.cornerRadius(6)
.padding(.bottom)
TextField("Write something...", text: $post)
.padding()
.background(Color.white)
.cornerRadius(6)
.padding(.bottom)
Spacer()
}.padding()
.alert(isPresented: $isAlert, content: {
let title = Text("No data")
let message = Text("Please fill your title and post")
return Alert(title: title, message: message)
})
}
.navigationBarTitle("New post", displayMode: .inline)
.navigationBarItems(leading: leading, trailing: trailing)
}
}
var leading: some View {
Button(action: {
isPresented.toggle()
}, label: {
Text("Cancel")
})
}
var trailing: some View {
Button(action: {
if title != "" && post != "" {
let parameters: [String: Any] = ["title": title, "post": post]
print(parameters)
viewModel.createPost(parameters: parameters)
viewModel.fetchPosts()
isPresented.toggle()
} else {
isAlert.toggle()
}
}, label: {
Text("Post")
})
}
}
ViewModel
class ViewModel: ObservableObject {
#Published var items = [PostModel]()
let prefixURL = "https://asreconnect.nl/CRM/php/app"
init() {
fetchPosts()
}
//MARK: - retrieve data
func fetchPosts() {
guard let url = URL(string: "\(prefixURL)/posts.php") else {
print("Not found url")
return
}
URLSession.shared.dataTask(with: url) { (data, res, error) in
if error != nil {
print(data!)
print("error", error?.localizedDescription ?? "")
return
}
do {
if let data = data {
print(data)
let result = try JSONDecoder().decode(DataModel.self, from: data)
DispatchQueue.main.async {
self.items = result.data
print(result)
}
} else {
print("No data")
}
} catch let JsonError {
print("fetch json error:", JsonError.localizedDescription)
print(String(describing: JsonError))
}
}.resume()
}
//MARK: - create data
func createPost(parameters: [String: Any]) {
guard let url = URL(string: "\(prefixURL)/create.php") else {
print("Not found url")
return
}
let data = try! JSONSerialization.data(withJSONObject: parameters)
print(data)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = data
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
URLSession.shared.dataTask(with: request) { (data, res, error) in
if error != nil {
print("error", error?.localizedDescription ?? "")
return
}
do {
if let data = data {
let result = try JSONDecoder().decode(DataModel.self, from: data)
DispatchQueue.main.async {
print(result)
}
} else {
print("No data")
}
} catch let JsonError {
print("fetch json error:", JsonError.localizedDescription)
}
}.resume()
}
}

How to go back to previous screen after getting response from API call in swiftui?

I am new in swift ui, I try to go back to previous screen after getting response from API call.
In my scenario, I have a button to call an API. This is my button,
import SwiftUI
struct RegisterCashierView: View {
#ObservedObject var registerCashierController = RegisterCashierController()
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
Button(action: {
if(self.registerCashierController.isActive){
self.presentationMode.wrappedValue.dismiss()
}
self.registerCashierController.submitRegisterCashier(email: self.email, full_name: self.full_name, username: self.username, password: self.password)
}) {
HStack {
Text("Submit")
.fontWeight(.semibold)
.font(.title)
Image(systemName: "arrow.right.circle")
.font(.title)
}
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
}
}
As we can see I have an isActive, when isActive is true, it will execute this
self.presentationMode.wrappedValue.dismiss()
I set my isActive to true variable after getting an response from API. But it is not work. Any one knows how to fix this?
class RegisterCashierController: ObservableObject {
#Published var isActive = false
#Published var isLoading = false
#State private var adminToken = UserDefaults.standard.string(forKey: "adminToken")
let url = BaseUrl().setUrl(subUrl: "auth/user/register")
var didChange = PassthroughSubject<RegisterCashierController, Never>()
func submitRegisterCashier(email: String, full_name: String, username: String, password: String){
let body : [String : String] = ["email": email, "full_name": full_name, "username": username, "password": password]
guard let finalBody = try? JSONEncoder().encode(body) else {
return
}
print("URL \(self.url)")
var request = URLRequest(url: self.url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(adminToken ?? "")", forHTTPHeaderField: "Authorization")
request.httpMethod = "POST"
request.httpBody = finalBody
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data, error == nil else {
print("No data response")
return
}
do {
let decoder = JSONDecoder()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
decoder.dateDecodingStrategy = .formatted(dateFormatter)
let response = try decoder.decode(RegisterCashierResponse.self, from: data)
DispatchQueue.main.async {
self.isActive = true
self.isLoading = false
}
print("Response from register cashier \(response)")
} catch let error {
print("Error from register cashier \(error)")
}
}.resume()
}
}
It works differently - you have to observe isActive, for example in onChange:
var body: some View {
Button(action: {
self.registerCashierController.submitRegisterCashier(email: self.email, full_name: self.full_name, username: self.username, password: self.password)
}) {
HStack {
Text("Submit")
.fontWeight(.semibold)
.font(.title)
Image(systemName: "arrow.right.circle")
.font(.title)
}
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.onReceive(registerCashierController.$isActive) { active in // SwiftUI 1.0+
// .onChange(of: registerCashierController.isActive) { active in // SwiftUI 2.0
if active {
self.presentationMode.wrappedValue.dismiss()
}
}
}

Async loading from url more than one image in SwiftUI

I'm trying to write a code that loads more than one image (between 5 and 10) from url. I use Theaudiodb api (loaded images are photos of artists). The problem is that only last image is displayed. In ContentView I have ForEach that loads between 5 and 10 ArtistImage(s) struct. I know that all images data are loaded (in ImageFetcher var imageData will set I put print(newValue) and it shows data loaded (for example: 148273 bytes ,etc). Just cannot understand why just the last image is displayed.
struct ArtistImage: View {
var imageURL: URL?
#ObservedObject private var imageFetcher: ImageFetcher
init(imageURL: String) {
self.imageURL = URL(string: imageURL)
imageFetcher = ImageFetcher(imageURL: self.imageURL)
}
var body: some View {
if let uiImage = UIImage(data: imageFetcher.imageData) {
return AnyView(Image(uiImage: UIImage(data: self.imageFetcher.imageData)!)
.renderingMode(.original)
.resizable()
.cornerRadius(10))
} else {
return AnyView(Text("Loading...")
.onAppear(perform: { self.imageFetcher.getImage() }))
}
}
and the ImageFetcher class below:
class ImageFetcher: ObservableObject {
public let objectWillChange = PassthroughSubject<Data,Never>()
public private(set) var imageData = Data() {
willSet {
print(newValue) // it tells me all images are loaded
objectWillChange.send(newValue)
}
}
var imageURL: URL?
public init(imageURL: URL?) {
self.imageURL = imageURL
}
func getImage() {
guard let url = imageURL else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
guard let data = data else {
return
}
DispatchQueue.main.async {
self.imageData = data
}
}.resume()
}
Content View:
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
init() {
networkManager.createArtistsDatabase()
}
var body: some View {
ScrollView(.vertical) {
GeometryReader { geo in
Button( action: {} ) {
ZStack(alignment: .bottom) {
Image("cp")
.renderingMode(.original)
.aspectRatio(contentMode: .fill)
.frame(width: geo.size.width, height: geo.size.height)
.clipped()
VStack(alignment: .center) {
Text("Featured artist").foregroundColor(.yellow).font(.custom("AvenirNext-Regular",size: 16)).padding(.bottom, 8)
Text(("Coldplay").uppercased()).foregroundColor(.white).font(.custom("AvenirNext-Bold", size:24))
}.frame(width: geo.size.width, height: 80).background(Color.black.opacity(0.5))
}
}
}.frame(height: 400.0)
VStack(alignment:.leading){
Text("Discover new artists").font(.custom("AvenirNext-Regular", size: 16)).padding(.top,10)
ScrollView(.horizontal) {
HStack {
ForEach(self.networkManager.artistsDB, id:\.self) { data in
Button(action:{} ) {
VStack {
ArtistImage(imageURL: data.artistImage)
.frame(width: 150, height: 150)
.cornerRadius(10)
Text(data.artistName).foregroundColor((.secondary)).font(.custom("AvenirNext-Regular", size: 14))
}
}
}
}.frame(height: 180)
}
}.frame(minWidth: 0, maxWidth:.infinity,maxHeight: .infinity, alignment: .leading).padding(.leading, 10)
}.edgesIgnoringSafeArea(.top)
}
}
Network Manager class:
class NetworkManager: ObservableObject {
#Published var artistsDB = [Artist]()
func createArtistsDatabase() {
let artistsNames: [String] = ["Madonna", "Metallica","Coldplay","Toto","Kraftwerk","Depeche%20Mode"]
for ar in artistsNames {
findArtistBy(name: ar)
}
}
func findArtistBy(id: String = "", name: String = "") {
var url: URL?
if id != "" {
guard let urlID = URL.getArtistByID(id: id) else { return }
url = urlID
} else {
guard let urlName = URL.getArtistByName(name: name) else { return }
url = urlName
}
let urlRequest = URLRequest(url: url!)
let task = URLSession.shared.dataTask(with: urlRequest) { data,response,error in
if error != nil || data == nil {
print("Client error")
return
}
guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
print("Server error")
return
}
guard let mime = response.mimeType, mime == "application/json" else {
print("Wrong mime type")
return
}
guard let dataToDecode = data else { return }
do {
let decoder = JSONDecoder()
let dataJSONed = try decoder.decode(DBArtist.self, from: dataToDecode)
DispatchQueue.main.async {
print(dataJSONed.artist[0])
self.artistsDB.append(dataJSONed.artist[0])
}
} catch {
print("Error while decoding!")
}
}
task.resume()
}
URL extension:
extension URL {
static func getArtistByID(id: String) -> URL? {
return URL(string: "https://theaudiodb.com/api/v1/json/1/artist.php?i=\((id))")
}
static func getArtistByName(name: String) ->URL? {
print("https://www.theaudiodb.com/api/v1/json/1/search.php?s=\(name)")
return URL(string: "https://www.theaudiodb.com/api/v1/json/1/search.php?s=\(name)")
}
}