I'm currently developing an application using SwiftUI.
I want to close a sheet after an API connection finish using a closure method.
So I tried to do that with the code below, but in the case of these codes, they don't work well...
How could I solve this problem?
Here are the codes:
TestSheet.swift
import SwiftUI
struct TestSheet: View {
#EnvironmentObject var appState: AppState
#State var id:Int = 1
#State var memo:String = "new Memo"
#State var isFinish:Bool = false
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView{
VStack{
Button(action: {
appState.makeUpDate(
pk:id, memo:memo, finish: {returnData in
isFinish = returnData
}
)
if(isFinish){
self.presentationMode.wrappedValue.dismiss()
}
})
{
Text("UPDATE")
}
}
}
}
}
AppState.swift
...
func makeUpDate(pk the_pk:Int, memo the_memo:String, finish:#escaping(Bool)->Void) {
var isFinish:Bool = false
let endpoint: String = "https://sample.com/api/info/\(the_pk)/"
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
return
}
var urlRequest = URLRequest(url: url)
urlRequest.addValue("token xxxxxxxxxx", forHTTPHeaderField: "authorization")
urlRequest.httpMethod = "PUT"
urlRequest.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
let newInfo:[String:Any]=["memo":the_memo]
let jsonInfo: Data
do {
jsonInfo = try JSONSerialization.data(withJSONObject: newInfo, options: [])
urlRequest.httpBody = jsonInfo
} catch {
print("Error: cannot create JSON from newInfo")
return
}
let session = URLSession.shared
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
guard error == nil else {
print("error calling Put")
print(error!)
return
}
guard let responseData = data else {
print("Error: did not receive data")
return
}
guard let response = response as? HTTPURLResponse else {
print("Error: did not response data")
return
}
print("The response code is \(response.statusCode)")
// parse the result as JSON, since that's what the API provides
do {
guard let receivedData = try JSONSerialization.jsonObject(with: responseData,
options: []) as? [String: Any] else {
print("Could not get JSON from responseData as dictionary")
return
}
print("The request is: " + receivedData.description)
} catch {
print("error parsing response from PUT")
return
}
DispatchQueue.main.async {
isFinish = true
finish(isFinish)
}
}
task.resume()
}
...
Xcode:Version 12.0.1
The isFinish is updated asynchronously, so we should react on it in different place
var body: some View {
NavigationView{
VStack{
Button(action: {
appState.makeUpDate(
pk:id, memo:memo, finish: {returnData in
isFinish = returnData
}
)
})
{
Text("UPDATE")
}
}
.onChange(of: isFinish) { result in
if result {
self.presentationMode.wrappedValue.dismiss() // << here !!
}
}
}
}
Related
Sorry for simple question, try to learn SwiftUI
My goal is to show alert then i can not load data from internet using .alert()
the problem is that my struct for error actually has data but it does not transfer to .alert()
debug shows that AppError struct fill in with error but then i try to check for nil or not it is always nil in .Appear()
PostData.swift
struct AppError: Identifiable {
let id = UUID().uuidString
let errorString: String
}
NetworkManager.swift
class NetworkManager: ObservableObject {
#Published var posts = [Post]()
#Published var appError: AppError? = nil
func fetchGuardData() {
if let url = URL(string: "http://hn.algolia.com/api/v1/search?tags=front_page") {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
if error == nil {
let decorder = JSONDecoder()
if let safeData = data {
do {
let results = try decorder.decode(Results.self, from: safeData)
DispatchQueue.main.sync {
self.posts = results.hits }
} catch {
self.appError = AppError(errorString: error.localizedDescription)
}
} else {
self.appError = AppError(errorString: error!.localizedDescription)
}
} else {
DispatchQueue.main.sync {
self.appError = AppError(errorString: error!.localizedDescription)
}
}
} //
task.resume()
} else {
self.appError = AppError(errorString: "No url response")
}
}
}
ContentView.swift
struct ContentView: View {
#StateObject var networkManager = NetworkManager()
#State var showAlert = false
var body: some View {
NavigationView {
List(networkManager.posts) { post in
NavigationLink(destination: DetailView(url: post.url)) {
HStack {
Text(String(post.points))
Text(post.title)
}
}
}
.navigationTitle("H4NEWS")
}
.onAppear() {
networkManager.fetchGuardData()
if networkManager.appError != nil {
showAlert = true
}
}
.alert(networkManager.appError?.errorString ?? "no data found", isPresented: $showAlert, actions: {})
}
}
Probably when doing this check, the data fetch process is not finished yet.
if networkManager.appError != nil {
showAlert = true
}
So you should wait the network request finish to check if there is error or not.
If you sure there is error and just test this try this to see error:
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
if networkManager.appError != nil {
showAlert = true
}
}
To handle better this situation you can pass a closure your fetchGuardData function and handle your result and error inside it.
or you can use .onChange for the listen the changes of appError.
.onChange(of: networkManager.appError) { newValue in }
I have 2 views - which I want to navigate between, and have a viewModel object shared between them as an EnvironmentObject. I keep getting the "A View.environmentObject(_:) for TidesViewModel may be missing as an ancestor of this view." error - but none of the solutions I have found seem to work. Please find below my code. The following is the first view:
import SwiftUI
struct ContentView: View {
#ObservedObject var tidesViewModel: TidesViewModel = TidesViewModel()
var body: some View {
NavigationView
{
List
{
ForEach (tidesViewModel.stations.indices) {
stationid in
HStack
{
NavigationLink(destination: TideDataView(stationId: tidesViewModel.stations[stationid].properties.Id))
{
Text(tidesViewModel.stations[stationid].properties.Name)
}
}
}
}
}.environmentObject(tidesViewModel)
}
}
and below is the child view - which throws the error.
import SwiftUI
struct TideDataView: View {
#EnvironmentObject var tidesViewModel : TidesViewModel
var stationId: String
init(stationId: String) {
self.stationId = stationId
getTidesForStation(stationId: stationId)
}
var body: some View {
List
{
ForEach (tidesViewModel.tides.indices)
{
tideIndex in
Text(tidesViewModel.tides[tideIndex].EventType)
}
}
}
func getTidesForStation(stationId: String)
{
tidesViewModel.getTidalData(forStation: stationId)
}
}
For completeness - below is the Observable object being passed:
import Foundation
import SwiftUI
class TidesViewModel: ObservableObject
{
private var tideModel: TideModel = TideModel()
var currentStation: Feature?
init()
{
readStations()
}
var stations: [Feature]
{
tideModel.features
}
var tides: [TidalEvent]
{
tideModel.tides
}
func readStations()
{
let stationsData = readLocalFile(forName: "stations")
parseStations(jsonData: stationsData!)
}
private func readLocalFile(forName name: String) -> Data? {
do {
if let bundlePath = Bundle.main.path(forResource: name,
ofType: "json"),
let jsonData = try String(contentsOfFile: bundlePath).data(using: .utf8) {
return jsonData
}
} catch {
print(error)
}
return nil
}
private func parseStations(jsonData: Data) {
do {
let decodedData: FeatureCollection = try JSONDecoder().decode(FeatureCollection.self,
from: jsonData)
//print(decodedData)
tideModel.features = decodedData.features
} catch let jsonError as NSError{
print(jsonError.userInfo)
}
}
func getTidalData(forStation stationId: String)
{
let token = "f43c068141bb417fb88909be5f68781b"
guard let url = URL(string: "https://admiraltyapi.azure-api.net/uktidalapi/api/V1/Stations/" + stationId + "/TidalEvents") else {
fatalError("Invalid URL")
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue(token, forHTTPHeaderField: "Ocp-Apim-Subscription-Key")
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else{ return }
do{
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64
let decodedData = try decoder.decode([TidalEvent].self, from: data)
DispatchQueue.main.async {
self.tideModel.tides = decodedData
}
}catch let error{
print(error)
}
}.resume()
}
}
You need to attach the modifier .environmentObject() directly to the view that will receive it. So, in your case, attach it to TideDataView, not to the NavigationView around it.
Your code would look like this:
NavigationLink {
TideDataView(stationId: tidesViewModel.stations[stationid].properties.Id)
.environmentObject(tidesViewModel)
} label: {
Text(tidesViewModel.stations[stationid].properties.Name)
}
// Delete the modifier .environmentObject() attached to the NavigationView
I found why the cache prompt will not be shown.
If I use the ImageView directly on the ContentView, the cache prompt will not show.
If I wrap the ImageView with a View, then use the wrapper view on the ContentView, the cache prompt will show.
Here is the working code in the ContentView.swift
struct ContentView: View {
var links =
[NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-0.jpg"),
NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-1.jpg"),
NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-2.jpg"),
NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-3.jpg"),
NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-4.jpg"),
NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-5.jpg"),
NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-6.jpg")]
var body: some View {
List(links) { news in
// working
NewsListItemView(item: news)
// not working
//NewsImageView(urlString: news.urlString)
}
}
}
This is the NewsListItemView which is just a wrapper
struct NewsListItemView: View {
var item: NewsItem
var body: some View {
NewsImageView(urlString: item.urlString)
}
}
This is my cache prompt location.
NewsImageViewModel.swift
class NewsImageViewModel: ObservableObject {
static var placeholder = UIImage(named: "NewsIcon.png")
#Published var image: UIImage?
var urlString: String?
init(urlString: String) {
self.urlString = urlString
loadImage()
}
func loadImage() {
if loadImageFromCache() {
return
}
loadImageFromURL()
}
func loadImageFromCache() -> Bool {
guard let cacheIamge = TemporaryImageCache.getShared()[urlString!] else {
return false
}
print("load from cache")
self.image = cacheIamge
return true
}
func loadImageFromURL() {
print("load from url")
guard let urlString = urlString else {
return
}
let url = URL(string: urlString)!
let task = URLSession.shared.dataTask(with: url, completionHandler: getResponseFromURL(data:response:error:))
task.resume()
}
func getResponseFromURL(data: Data?, response: URLResponse?, error: Error?) {
guard error == nil else {
print("Error \(error!)")
return
}
guard data != nil else {
print("No founded data")
return
}
DispatchQueue.main.async {
guard let loadedImage = UIImage(data: data!) else {
print("Not supported data ")
return
}
self.image = loadedImage
TemporaryImageCache.getShared().cache.setObject(loadedImage, forKey: self.urlString! as NSString)
}
}
}
NewsImageView.swift
import SwiftUI
struct NewsImageView: View {
#ObservedObject var model: NewsImageViewModel
init(urlString: String) {
model = NewsImageViewModel(urlString: urlString)
}
var body: some View {
Image(uiImage: model.image ?? NewsImageViewModel.placeholder!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100, alignment: .center)
}
}
NewsListItemView.swift
struct NewsListItemView: View {
var item: NewsItem
var body: some View {
NewsImageView(urlString: item.urlString)
}
}
This is ImageCache.swift
protocol ImageCache {
subscript(_ urlString: String) -> UIImage? {get set }
}
struct TemporaryImageCache: ImageCache {
subscript(urlString: String) -> UIImage? {
get {
cache.object(forKey: urlString as NSString)
}
set {
newValue == nil ? cache.removeObject(forKey: urlString as NSString) : cache.setObject(newValue!, forKey: urlString as NSString)
}
}
var cache = NSCache<NSString, UIImage>()
}
extension TemporaryImageCache {
private static var shared = TemporaryImageCache()
static func getShared() -> TemporaryImageCache {
return shared
}
}
This is NewsItem.swift
struct NewsItem: Identifiable {
var id = UUID()
var urlString: String
}
I'm currently developing an application using SwiftUI.
I want to show two kinds of data in two lists in a view.
(1)start_date
(2)the temperatures of each day from start_date(1) to today
in the case of my code, (1)start_date is shown well, but (2)the temperatures have a problem because each list should show different data but they show the same data in each list...
Although I can check when each method is called, they make deferent data in the console like below, the simulator shows the same data...
The result in the console:
temp_info
25.7
24.9
temp_info
25.6
25.7
24.9
24.1
23.5
25.7
26.4
23.7
23.0
24.4
26.1
How could I resolve this problem?
Here are the codes:
JsonModel.swift
import Foundation
struct DbVegetableInfos: Codable,Identifiable {
var id: Int
var start_date: String
}
struct WeatherAveInfos:Codable,Identifiable {
var id: Int
var ave_temp: Float
}
AppState.swift
import SwiftUI
import Foundation
import Combine
import UIKit
class AppState: ObservableObject {
#Published var arrayDbVegetableInfos:[DbVegetableInfos]?
#Published var weatherAveInfos:[WeatherAveInfos]?
func makeGetCallVegetableInfos() {
// Set up the URL request
let endpoint: String = "https://sample.com/api/info/"
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
return
}
var urlRequest = URLRequest(url: url)
urlRequest.addValue("token xxxxxxxxxx", forHTTPHeaderField: "authorization")
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// check for any errors
guard error == nil else {
print("error calling GET")
print(error!)
return
}
// make sure we got data
guard let responseData = data else {
print("Error: did not receive data")
return
}
// parse the result as JSON, since that's what the API provides
DispatchQueue.main.async {
do{ self.arrayDbVegetableInfos = try JSONDecoder().decode([DbVegetableInfos].self, from: responseData)
}catch{
print("Error: did not decode")
return
}
}
}
task.resume()
}
}
func makeGetCallWeatherAveTemp(start_date:String ) {
// Set up the URL request
let endpoint: String = "https://sample.com/api/weather_ave/?start_date=\(start_date)"
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
return
}
var urlRequest = URLRequest(url: url)
urlRequest.addValue("token xxxxxxxxxx", forHTTPHeaderField: "authorization")
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// check for any errors
guard error == nil else {
print("error calling GET")
return
}
// make sure we got data
guard let responseData = data else {
print("Error: did not receive data")
return
}
// parse the result as JSON, since that's what the API provides
DispatchQueue.main.async {
do{ self.weatherAveInfos = try JSONDecoder().decode([WeatherAveInfos].self, from: responseData)
print("temp_info")
for info in self.weatherAveInfos!{
print(info.ave_temp)
}
}catch{
print("Error: did not decode")
return
}
}
}
task.resume()
}
HomeView.swift
import SwiftUI
struct HomeView: View {
#EnvironmentObject var appState: AppState
var body: some View {
NavigationView{
VStack{
ForEach(appState.arrayDbVegetableInfos ?? []){ info in
VStack{
VegetableInfoRow(info:info)
}.background(Color(.secondarySystemFill))
.cornerRadius(10)
.padding(.top)
.padding(.leading)
.padding(.bottom)
}
}.onAppear(){
appState.makeGetCallVegetableInfos()
}
}
}
}
VegetableInfoRow.swift
import SwiftUI
struct VegetableInfoRow: View {
#EnvironmentObject var appState: AppState
var info:DbVegetableInfos
var body: some View {
ScrollView(.horizontal) {
HStack{
VStack{
VStack{
Text("start_dateļ¼").padding()
Text(stringToStringDate(stringDate: info.start_date, format: "yyyy-MM-dd"))
}
}
Divider()
.padding()
VStack{
VStack{
Text("progress_temperatureļ¼").padding()
ForEach(appState.weatherAveInfos ?? []){ info in
Text(String(info.ave_temp))
}
}
}
}
}.onAppear(){
appState.makeGetCallWeatherAveTemp(start_date: info.start_date)
}
}
}
func stringToStringDate(stringDate: String, format:String) -> String {
let formatter: DateFormatter = DateFormatter()
formatter.calendar = Calendar(identifier: .gregorian)
formatter.dateFormat = "yyyy/MM/dd"
let newDate = formatter.date(from: stringDate)!
formatter.dateFormat = format
return formatter.string(from: newDate)
}
Xcode:Version 12.0.1
I have the below code. I got print "OK Call" before to print (finalData). I know the reason is URLSession goes in parallel, but my question is: How could avoid the parallel task and wait until URLSession ends ? THANKS
import SwiftUI
struct ContentView: View {
var body: some View {
Button ("Action", action: {
self.checkLogin(username:"test", password:"123456")
print ("OK Call")
} ) }
func checkLogin (username: String, password: String) {
var body: [String:String] = [:]
guard let url = URL(string: "http://test/apple/login.php") else { return }
body = ["user": username, "password": password]
let finalBody = try! JSONEncoder().encode (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
if let error = error { print ("Error: \(error)")
return
}
if let data = data {
let finalData = try! JSONDecoder().decode(ServerMessage.self, from: data)
print (finalData)
return
}
}.resume()
}
}
struct ServerMessage: Decodable {
let result: String
let nuser: String
}