I'm new to SwiftUI. Currently struggling with fetching and presenting photos and places names to my List from Google Places (Maps) API. I do know that they provide explanation of how to do it, but since I'm not that experienced yet, i cannot get the code provided just for Swift (not Swift UI). Tried to use the method below:
struct Response: Codable {
var results: [Place]
}
struct Place: Codable {
var placeId: Int
var placeName: String
var placePic: String
var placeTime: String
var placePhone: String
var placeLocation: String
}
#State var results = [Place] ()
var body: some View {
List(results, id: \.placeId) { item in
VStack{
Text(item.placeName)
}
}.onAppear(perform: loadData)
}
func loadData(){
guard let url = URL(string: "https://maps.googleapis.com/maps/api/place/photo?parameters") else{
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request){data, responce, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
DispatchQueue.main.async {
self.results = decodedResponse.results
}
return
}
}
print("fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}
Question:
Is there any ideas of what i do wrong? Because I tried to search for some tutorials on the web, using SwiftUI, but haven't found any.
Related
I have a json-result that I get from my own "api". In this I have a list of different devices, that I like to view in a list.
When I add the ForEach, then I get the following error:
Generic struct 'List' requires that 'some AccessibilityRotorContent' conform to 'View'
The JsonResponse:
[{"name":"Tormek-T8","topHorizontal":55,"topVertical":55,"frontHorizontal":44,"frontVertical":44},{"name":"SH-332","topHorizontal":77,"topVertical":77,"frontHorizontal":88,"frontVertical":88}]
Can it be, that there are numbers in this JSON-String and not all is a String?
In my very simple code I had made a struct for the single Device and try to pull it from the url. Why I would get this error? Because the response of the JSON is not nil.
struct Device: Hashable, Codable {
let name: String
let topHorizontal: Double
let topVertical: Double
let frontHorizontal: Double
let frontVertical: Double
}
class ViewModel: ObservableObject {
#Published var devices: [Device] = []
func fetch() {
guard let url = URL(string: "https://cdn.rowoco.de/grindcalculator/devices") else {
return
}
let task = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
guard let data = data, error == nil else {
return
}
do {
let devices = try JSONDecoder().decode([Device].self, from: data)
DispatchQueue.main.async {
self?.devices = devices
}
} catch {
print(error)
}
}
task.resume()
}
}
struct CdnDevicesListView: View {
#Environment(\.presentationMode) var presentationMode
#StateObject var viewModel = ViewModel()
var body: some View {
NavigationView {
List {
ForEach(viewModel.devices, id: \.self) { device in
device.name
}
}
.navigationTitle("cdn devices")
.onAppear {
viewModel.fetch()
}
}
}
}
ForEach's content needs to be a View.
device.name is not a View.
This is my Json file and i don't understand how to fetch data and set
the Image in our SwiftUI code. please help me resolve this problem.
And this is My Model, is this model correct?
This is My API and wants to fetch only value images array
import Foundation
public struct BannerImages {
public let images: [String]
public init(images: [String]) {
self.images = images
}
}
try this approach to fetch your images and display them in a View:
import Foundation
import SwiftUI
struct ContentView: View {
#StateObject var vm = ViewModel()
var body: some View {
VStack {
Text("Fetching the data...")
List (vm.images, id: \.self) { url in
AsyncImage(url: URL(string: url)) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 111, height: 111)
} placeholder: {
ProgressView()
}
}
}
.task {
await vm.getData()
}
}
}
class ViewModel: ObservableObject {
#Published var images = [String]()
func getData() async {
guard let url = URL(string: "apiurl") else { return }
do {
let (data, _) = try await URLSession.shared.data(from: url)
Task{#MainActor in
let results = try JSONDecoder().decode(APIResponse.self, from: data)
self.images = results.images
}
} catch {
print("---> error: \(error)")
}
}
}
struct APIResponse: Codable {
let images: [String]
}
1. First you need to have a variable with data type of Data like this: var imageData: Data?
2. Then you have to fetch the image data from the link in the array like this:
func getImageData() {
// Check image url isnt nill
guard imageUrl(your image url) != nil else {
return
}
// Download the data for the image
let url = URL(string: imageUrl(your image url)!)
if let url = url {
let session = URLSession.shared
let dataTask = session.dataTask(with: url) { data, response, error in
if error == nil {
DispatchQueue.main.async {
self.imageData = data
}
}
}
dataTask.resume()
}
}
3. Once this is done go to the view file where you want to display the image and create
let uiImage = UIImage(data: put the var in which you stored the image data in previous step ?? Data())
Image(uiImage: uiImage ?? UIImage())
I am currently using an api to grab the definitions for a specific word that the user has entered, and the api returns multiple definitions. I want the user to be able to choose what exact definition they want to pair a word with. Since I am interacting with an api, it is in a function and I cannot return anything out of it. I want to grab all the definitions and then show a new view where the user can pick the appropriate definition. How can I go about doing this? I've thought of making an ObservableObject that just has an array as a work around, but that seems a bit excessive. I am new to SwiftUI, so I am unsure whether or not this would be possible. However, I think it would not be because I am not trying to return a view anywhere or using any of the built in things that accepts views.
EDIT: I made SaveArray an ObservableObject and now my problem is that the object is not being updated by my getDef function call. Within the function it is but it is not editing the actual class or at least that is what it is looking like, because on my next view I have a foreach going through the array and nothing is displayed because it is empty. I am not sure whether that is because the sheet is being brought up before the getDef function is done executing.
struct AddWord: View {
#ObservedObject var book: Book
#ObservedObject var currentArray = SaveArray()
#State var addingDefinition = false
#State var word = ""
#State var definition = ""
#State var allDefinitions: [String] = []
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
Form {
TextField("Word: ", text: $word)
}
.navigationBarTitle("Add word")
.navigationBarItems(trailing: Button("Add") {
if self.word != "" {
book.words.append(self.word)
getDef(self.word, book, currentArray)
addingDefinition = true
self.presentationMode.wrappedValue.dismiss()
}
}).sheet(isPresented: $addingDefinition) {
PickDefinition(definitions: currentArray, book: book, word: self.word)
}
}
}
func getDef(_ word: String, _ book: Book, _ definitionsArray: SaveArray) {
let request = NSMutableURLRequest(url: NSURL(string: "https://wordsapiv1.p.rapidapi.com/words/\(word)")! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
print(error)
} else {
do {
let dictionary = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String:Any]
//getting the dictionary
let dict = dictionary?["results"] as? [Any]
definitionsArray.currentArray = createArray((dict!))
}
catch {
print("Error parsing")
}
}
})
dataTask.resume()
}
func createArray(_ array: [Any]) -> [String] {
//Get all the definitions given from the api and put it into a string array so you can display it for user to select the correct definiton for their context
var definitions = [String]()
for object in array {
let dictDef = object as? [String: Any]
definitions.append(dictDef?["definition"] as! String)
}
return definitions
}
}
struct AddWord_Previews: PreviewProvider {
static var previews: some View {
AddWord(book: Book())
}
}
struct PickDefinition: View {
#ObservedObject var definitions: SaveArray
var book: Book
var word: String
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack {
ForEach(0 ..< definitions.currentArray.count) { index in
Button("\(self.definitions.currentArray[index])", action: {
print("hello")
DB_Manager().addWords(name: self.book.name, word: self.word, definition: self.definitions.currentArray[index])
book.definitions.append(self.definitions.currentArray[index])
self.presentationMode.wrappedValue.dismiss()
})
}
}
.navigationTitle("Choose")
}
}
}
struct PickDefinition_Previews: PreviewProvider {
static var previews: some View {
PickDefinition(definitions: SaveArray(), book: Book(), word: "")
}
}
If you can post more of your code, I can provide a fully working example (e.g. the sample JSON and the views/classes you have built). But for now, I am working with what you provided. I hope the below will help you see just how ObservableObject works.
#Published var dict = [String]() //If the api returns a list of strings, you can make this of type string - I do not have a sample of the JSON so I cannot be sure. If you can provide a sample of the JSON I can better define the way this should work.
var body: some View {
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
print(error)
} else {
do {
let dictionary = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String:Any]
//getting the dictionary
dict = dictionary?["results"] as? [Any] //note that here we are assigning the results of the api call to the Published variable, which our StateObject variable in ContentView is listening to!
var allDef = createArray((dict!))
//No longer need to pass this data forward (as you have below) since we are publishing the information!
//Pass the array to the new view where the user will select the one they want
PickDefinition(definitions: allDef, book: self.book, word: self.word)
}
catch {
print("Error parsing")
}
}
})
dataTask.resume()
}
}
struct ContentView: View {
//StateObject receives updates sent by the Published variable
#StateObject var dictArray = Api()
var body: some View {
NavigationView {
List {
ForEach(dictArray.dict.indices, id: \.self) { index in
Text(dictArray.dict[index])
}
}
}
}
}
I have a currency API that returns a JSON object containing a strange arrangement: the base currency is used as a label. Typical currency APIs have labels like "base", "date", "success", and "rates", but this API doesn't have any of those.
{
"usd": {
"aed": 4.420217,
"afn": 93.3213,
"all": 123.104693,
"amd": 628.026474,
"ang": 2.159569,
"aoa": 791.552347,
"ars": 111.887966,
"aud": 1.558363,
"awg": 2.164862,
"azn": 2.045728,
"bam": 1.9541,
"bbd": 2.429065,
"bch": 0.001278
}
}
The "usd" (US dollars) at the top is called the base or home currency. At the moment the storage structure and state parameter are hardcoded to "usd" which prevents using exchange rates with other base currencies. The exchange rate API works great for a base currency of US dollars.
I need help modifying things so that I can download the exchange rates with different base currencies. For example, can I use a string variable in the storage model and state parameter? Any enlightenment will be greatly appreciated.
struct RateResult: Codable {
let usd: [String: Double]
}
#State private var results = RateResult(usd: [:])
struct ContentView: View {
var body: some View {
}
func UpdateRates() {
let baseUrl = "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api#1/latest/currencies/"
let baseCur = baseCurrency.baseCur.baseS // usd
let requestType = ".json"
guard let url = URL(string: baseUrl + baseCur + requestType) 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 decodedResponse = try? JSONDecoder().decode(RateResult.self, from: data) {
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
self.results = decodedResponse
// save off currency exchange rates
}
// everything is good, so we can exit
return
}
}
// if we're still here it means there was a problem
print("Currency Fetch Failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}
import SwiftUI
//You can't use the standard Codable for this. You have to make your own.
class BaseCurrency: Codable {
let id = UUID()
var baseCurrencies: [String : [String: Double]] = [:]
required public init(from decoder: Decoder) throws {
do{
print(#function)
let baseContainer = try decoder.singleValueContainer()
let base = try baseContainer.decode([String : [String: Double]].self)
for key in base.keys{
baseCurrencies[key] = base[key]
}
}catch{
print(error)
throw error
}
}
//#State should never be used outside a struct that is a View
}
struct CurrencyView: View {
#StateObject var vm: CurrencyViewModel = CurrencyViewModel()
var body: some View {
VStack{
List{
if vm.results != nil{
ForEach(vm.results!.baseCurrencies.sorted{$0.key < $1.key}, id: \.key) { key, baseCurrency in
DisclosureGroup(key){
ForEach(baseCurrency.sorted{$0.key < $1.key}, id: \.key) { key, rate in
HStack{
Text(key)
Text(rate.description)
}
}
}
}
}else{
Text("waiting...")
}
}
//To select another rate to go fetch
RatesPickerView().environmentObject(vm)
}.onAppear(){
vm.UpdateRates()
}
}
}
struct RatesPickerView: View {
#EnvironmentObject var vm: CurrencyViewModel
var body: some View {
if vm.results != nil{
//You can probaly populate this picker with the keys in
// baseCurrency.baseCur.baseS
Picker("rates", selection: $vm.selectedBaseCurrency){
ForEach((vm.results!.baseCurrencies.first?.value.sorted{$0.key < $1.key})!, id: \.key) { key, rate in
Text(key).tag(key)
}
}
}else{
Text("waiting...")
}
}
}
class CurrencyViewModel: ObservableObject{
#Published var results: BaseCurrency?
#Published var selectedBaseCurrency: String = "usd"{
didSet{
UpdateRates()
}
}
init() {
//If you can .onAppear you don't need it here
//UpdateRates()
}
func UpdateRates() {
print(#function)
let baseUrl = "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api#1/latest/currencies/"
let baseCur = selectedBaseCurrency // usd
let requestType = ".json"
guard let url = URL(string: baseUrl + baseCur + requestType) else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
do{
let decodedResponse = try JSONDecoder().decode(BaseCurrency.self, from: data)
DispatchQueue.main.async {
if self.results == nil{
//Assign a new base currency
self.results = decodedResponse
}else{ //merge the existing with the new result
for base in decodedResponse.baseCurrencies.keys{
self.results?.baseCurrencies[base] = decodedResponse.baseCurrencies[base]
}
}
//update the UI
self.objectWillChange.send()
}
}catch{
//Error thrown by a try
print(error)//much more informative than error?.localizedDescription
}
}
if error != nil{
//data task error
print(error!)
}
}.resume()
}
}
struct CurrencyView_Previews: PreviewProvider {
static var previews: some View {
CurrencyView()
}
}
I've got a SwiftUI View that takes a Hacker News API submission ID, and then fetches the details for that item in fetchStory().
When fetchStory() completed its HTTP call, it updates the #State private var url on the View, however the View never re-renders to show the new value -- it always shows the initial empty value.
Why?
import Foundation
import SwiftUI
struct StoryItem: Decodable {
let title: String
let url: String?
}
struct StoryView: View {
public var _storyId: Int
#State private var url: String = "";
init(storyId: Int) {
self._storyId = storyId
self.fetchStory()
}
var body: some View {
VStack {
Text("(Load a webview here for the URL of Story #\(self._storyId))")
Text("URL is: \(self.url)") // this never changes!
}
}
private func fetchStory() {
let url = URL(string: "https://hacker-news.firebaseio.com/v0/item/\(self._storyId).json")!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else {
print(String(error.debugDescription))
return
}
do {
let item: StoryItem = try JSONDecoder().decode(StoryItem.self, from: data)
if let storyUrl = item.url {
self.url = storyUrl
} else {
print("No url")
}
} catch {
print(error)
}
}
task.resume()
}
}
struct StoryView_Previews: PreviewProvider {
static var previews: some View {
StoryView(storyId: 22862053)
}
}
something like this:
import SwiftUI
struct StoryItem: Decodable {
let title: String
let url: String?
}
class ObservedStoryId: ObservableObject {
#Published var storyId: String = ""
init(storyId: String) {
self.storyId = storyId
}
}
struct StoryView: View {
#ObservedObject var storyId: ObservedStoryId
#State var url: String = ""
var body: some View {
VStack {
Text("(Load a webview here for the URL of Story #\(self.storyId.storyId))")
Text("URL is: \(self.url)")
Text(self.storyId.storyId)
}.onReceive(storyId.$storyId) { _ in self.fetchStory() }
}
private func fetchStory() {
let url = URL(string: "https://hacker-news.firebaseio.com/v0/item/\(self.storyId.storyId).json")!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else {
print(String(error.debugDescription))
return
}
do {
let item: StoryItem = try JSONDecoder().decode(StoryItem.self, from: data)
if let storyUrl = item.url {
self.url = storyUrl
} else {
print("No url")
}
} catch {
print(error)
}
}
task.resume()
}
}
and call it like this:
struct ContentView: View {
#ObservedObject var storyId = ObservedStoryId(storyId: "22862053")
var body: some View {
StoryView(storyId: storyId)
}
}
to fix your problem:
init(storyId: Int) {
self._storyId = storyId
}
var body: some View {
VStack {
Text("(Load a webview here for the URL of Story #\(self._storyId))")
Text("URL is: \(self.url)") // this now works
}.onAppear(perform: fetchStory)
}
why does this works and not your code,
my guess is this: "self.url" can only be updated/changed within the special SwiftUI View functions, such as onAppear(), elsewhere it does not change a thing.