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 ?? ""
}
}
}
Related
I've been trying to get a list of entities from a graphql endpoint but I can't figure it out. Also, the console in my Xcode v13.4 isn't showing anything even though I have some print() statements in the code, so that's not helping - I've found where it is at the bottom of the window but it's always blank.
My View to get the data is below, the DetailView is the link following a link from the main ContentView. the loadData function content, I got the code from Postman after testing the graphql call.
import SwiftUI
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
//array of properties
struct Response: Codable {
var data: Properties
}
struct Properties: Codable {
var properties: Nodes
}
struct Nodes: Codable {
var nodes: [Result]
}
struct Result: Codable {
var propertyId: Int
var title: String
}
struct DetailView: View {
#State private var results = [Result]()
var body: some View {
List(results, id: \.propertyId) { property in
VStack(alignment: .leading) {
Text(property.title)
.font(.headline)
}
}
.task{
await loadData()
}
}
func loadData() async {
let semaphore = DispatchSemaphore (value: 0)
let parameters = "{\"query\":\"{\\n properties {\\n nodes {\\n title(format: RENDERED)\\n propertyId\\n }\\n }\\n}\",\"variables\":{}}"
let postData = parameters.data(using: .utf8)
var request = URLRequest(url: URL(string: "http://DOMAIN/graphql")!,timeoutInterval: Double.infinity)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = postData
do {
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
print(String(describing: error))
semaphore.signal()
return
}
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
results = decodedResponse.data.properties.nodes
}
semaphore.signal()
}
task.resume()
semaphore.wait()
} catch {
print("Invalid data")
}
}
}
The output of the graphql call is
{
"data": {
"properties": {
"nodes": [
{
"title": "MY TITLE",
"propertyId": 00001
}
]
}
}
}
EDIT
Swapped try? for try! as suggested
if let decodedResponse = try! JSONDecoder().decode(Response.self, from: data) {
results = decodedResponse.data.properties.nodes
}
error: Initializer for conditional binding must have Optional type, not 'Response'
I managed to get it all working with the below...
import SwiftUI
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
//array of properties
struct Response: Codable {
var data: Properties
}
struct Properties: Codable {
var properties: Nodes
}
struct Nodes: Codable {
var nodes: [Result]
}
struct Result: Codable {
var propertyId: Int
var title: String
}
struct DetailView: View {
#State private var results = [Result]()
#State private var test = "one"
var body: some View {
List(results, id: \.propertyId) { property in
VStack(alignment: .leading) {
Text(property.title)
.font(.headline)
}
}.task{
await loadData()
}
}
func loadData() async {
let semaphore = DispatchSemaphore (value: 0)
let parameters = "{\"query\":\"{\\n properties {\\n nodes {\\n title(format: RENDERED)\\n propertyId\\n }\\n }\\n}\",\"variables\":{}}"
let postData = parameters.data(using: .utf8)
var request = URLRequest(url: URL(string: "https://DOMAIN/graphql")!,timeoutInterval: Double.infinity)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = postData
do {
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
print(String(describing: error))
semaphore.signal()
return
}
let decodedResponse = try! JSONDecoder().decode(Response.self, from: data)
results = decodedResponse.data.properties.nodes
semaphore.signal()
}
task.resume()
semaphore.wait()
} catch {
print("Invalid data \(error)")
}
}
}
Much to learn :)
I have a small problem. Im making a post request to log in, the "authenticated" is set to "true" when the request is succesful but the view is not changing.
When i'm pressing the Log In button, i'm calling this function :
#Published var authenticatedAngajator = false
func postAuthAngajator(email: String, password: String) {
guard let url = URL(string: postUrl) else { return }
let body: [String: Any] = ["email": email, "password": password, "acctype" : "angajator"]
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = body.percentEncoded()
request.setValue("application/x-www-form-urlencoded", 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.result)
if resData.result == "success" {
DispatchQueue.main.async {
self.authenticatedAngajator = true
}
}
}.resume()
}
And this is my content view, but when the request is called, the view is not changing and i do not know why. This is my content view :
struct ContentView: View {
#EnvironmentObject var userAuth : HttpAuth
var body: some View {
if userAuth.authenticatedNevazator {
MainViewNevazator()
}
if userAuth.authenticatedAngajator {
MainViewAngajator()
}
if userAuth.authenticatedUniversitate {
MainViewUniversitate()
}
else
{
MainView()
}
}
}
This is where postAuthAngajator is called.
struct LoginViewNevazator: View {
#State var emailNevazator: String = ""
#State var passwordNevazator: String = ""
#ObservedObject var manager = HttpAuth()
var body: some View {
ZStack {
VStack {
UserImage()
UsernameTextField(username: $emailNevazator)
PasswordSecureField(password: $passwordNevazator)
Button(action: {
manager.postAuthNevazator(email: self.emailNevazator, password: self.passwordNevazator)
}) {
LoginButtonContent()
}
What can i do in this situation ?
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()
}
}
This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 1 year ago.
First of all, this is my first attempt at Swift so I'm not really sure what I'm doing. I'm learning as I go right now and have hit a roadblock.
I'm trying to implement a WatchOS app that will call an API on a set timer to track fluctuations in some crypto prices.
I have figured out how to make the API call and get the JSON parsed to a point where I can print the data but I'm struggling to get it out of the closure and to my interface. I know the proper way to do this is with a completion handler but I can't seem to get a solid understanding of how to make that work in this scenario.
Any help would be appreciated
import SwiftUI
var refresh = bitcoin()
var btc: String = refresh
var eth: String = "ETH"
var doge: String = "DOGE"
struct ContentView: View {
var body: some View {
VStack(alignment: .leading ){
Label("\(btc)", image: "eth").padding(.vertical, 10.0)
Label("\(eth)", image: "eth").padding(.vertical, 10.0)
Label("\(doge)", image: "doge").padding(.vertical, 10.0)
}
.scaledToFill()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
struct responseData: Codable{
let data: Response?
}
struct Response: Codable{
var id: String
var rank: String
var symbol: String
var name: String
var supply: String
var maxSupply: String
var marketCapUsd: String
var volumeUsd24Hr: String
var priceUsd: String
var changePercent24Hr: String
var vwap24Hr: String
}
func bitcoin() -> String{
var result: String = "btc"
var request = URLRequest(url: URL(string: "https://api.coincap.io/v2/assets/bitcoin")!,timeoutInterval: Double.infinity)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
print(String(describing: error))
return
}
let response = try! JSONDecoder().decode(responseData.self, from: data)
result = (response.data?.priceUsd)!
print(result)
}
task.resume()
return result
}
There many ways to achieve what you want, one way is to use "ObservableObject". Try something like this:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class CoinModel: ObservableObject {
#Published var btcPriceUsd = "not yet available"
#Published var ethPriceUsd = "not yet available"
#Published var dogePriceUsd = "not yet available"
}
struct ContentView: View {
#StateObject var coins = CoinModel()
var body: some View {
VStack(alignment: .leading ){
Label("\(coins.btcPriceUsd)", image: "btc").padding(.vertical, 10.0)
Label("\(coins.ethPriceUsd)", image: "eth").padding(.vertical, 10.0)
Label("\(coins.dogePriceUsd)", image: "doge").padding(.vertical, 10.0)
}
.scaledToFill()
.onAppear {
// bitcoin()
bitcoin2 { price in
coins.btcPriceUsd = price
}
}
}
func bitcoin() {
var request = URLRequest(url: URL(string: "https://api.coincap.io/v2/assets/bitcoin")!,timeoutInterval: Double.infinity)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
return
}
let response = try! JSONDecoder().decode(responseData.self, from: data)
if let respData = response.data {
DispatchQueue.main.async {
coins.btcPriceUsd = respData.priceUsd
}
}
}
task.resume()
}
}
EDIT: if you really want to use completion, then try this:
func bitcoin2(completion: #escaping (String) -> Void) {
var request = URLRequest(url: URL(string: "https://api.coincap.io/v2/assets/bitcoin")!,timeoutInterval: Double.infinity)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
return completion("")
}
let response = try! JSONDecoder().decode(responseData.self, from: data)
if let respData = response.data {
DispatchQueue.main.async {
completion(respData.priceUsd)
}
}
}
task.resume()
}
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()
}
}
}