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()
}
}
Related
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 ?? ""
}
}
}
I have been trying to asynchronously load an image in my app using combine. Currently all the other pieces of data are loading fine, but my image seem to be stuck in a progress view. Why? I am not too familiar with how combine works as I have been trying to follow a tutorial and adapting it to fit my needs, which is why I ran into this problem.
This is my code:
Main View:
import SwiftUI
struct ApodView: View {
#StateObject var vm = ApodViewModel()
var body: some View {
ZStack {
// Background Layer
Color.theme.background
.ignoresSafeArea()
// Content Layer
VStack() {
Text(vm.apodData?.title ?? "Placeholder")
.font(.title)
.fontWeight(.bold)
.multilineTextAlignment(.center)
.foregroundColor(Color.theme.accent)
ApodImageView(apodData: vm.apodData ?? ApodModel(date: "", explanation: "", url: "", thumbnailUrl: "", title: ""))
ZStack() {
Color.theme.backgroundTextColor
ScrollView(showsIndicators: false) {
Text(vm.apodData?.explanation ?? "Loading...")
.font(.body)
.fontWeight(.semibold)
.foregroundColor(Color.theme.accent)
.multilineTextAlignment(.center)
.padding()
}
}
.cornerRadius(10)
}
.padding()
}
}
}
ImageView:
import SwiftUI
struct ApodImageView: View {
#StateObject var vm: ApodImageViewModel
init(apodData: ApodModel) {
_vm = StateObject(wrappedValue: ApodImageViewModel(apodData: apodData))
}
var body: some View {
ZStack {
if let image = vm.image {
Image(uiImage: image)
.resizable()
.scaledToFit()
} else if vm.isLoading {
ProgressView()
} else {
Image(systemName: "questionmark")
.foregroundColor(Color.theme.secondaryText)
}
}
.frame(maxWidth: .infinity, maxHeight: 250)
.cornerRadius(10)
}
}
Image ViewModel:
import Foundation
import SwiftUI
import Combine
class ApodImageViewModel: ObservableObject {
#Published var image: UIImage?
#Published var isLoading: Bool = false
private let apodData: ApodModel
private let dataService: ApodImageService
private var cancellables = Set<AnyCancellable>()
init(apodData: ApodModel) {
self.apodData = apodData
self.dataService = ApodImageService(apodData: apodData)
self.addSubscribers()
self.isLoading = true
}
private func addSubscribers() {
dataService.$image
.sink { [weak self] _ in
self?.isLoading = false
} receiveValue: { [weak self] returnedImage in
self?.image = returnedImage
}
.store(in: &cancellables)
}
}
Networking For Image:
import Foundation
import SwiftUI
import Combine
class ApodImageService: ObservableObject {
#Published var image: UIImage?
private var imageSubscription: AnyCancellable?
private let apodData: ApodModel
init(apodData: ApodModel) {
self.apodData = apodData
getApodImage()
}
private func getApodImage() {
guard let url = URL(string: apodData.thumbnailUrl ?? apodData.url) else { return }
imageSubscription = NetworkingManager.download(url: url)
.tryMap({ data -> UIImage? in
return UIImage(data: data)
})
.sink(receiveCompletion: NetworkingManager.handleCompletion, receiveValue: { [weak self] returnedImage in
self?.image = returnedImage
self?.imageSubscription?.cancel()
})
}
}
General Networking Code:
import Foundation
import Combine
class NetworkingManager {
enum NetworkingError: LocalizedError {
case badURLResponse(url: URL)
case unknown
var errorDescription: String? {
switch self {
case .badURLResponse(url: let url): return "Bad response from URL: \(url)"
case .unknown: return "Unknown Error Returned"
}
}
}
static func download(url: URL) -> AnyPublisher<Data, Error> {
return URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .background))
.tryMap({ try handleURLResponse(output: $0, url: url) })
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
Where you have:
private func addSubscribers() {
dataService.$image
.sink { [weak self] _ in
self?.isLoading = false
} receiveValue: { [weak self] returnedImage in
self?.image = returnedImage
}
.store(in: &cancellables)
}
You are subscribing to the published value of the image property. That image property stream will never complete. It is an infinite sequence tracking the value of that property over time "forever".
I don't think your receiveCompletion will ever be called so self?.isLoading = false will never happen.
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()
}
}
}
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)")
}
}
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
}