Cant view the list of devices - swiftui

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.

Related

SwiftUI Object details on list click

Building a crystal app. Displaying a list, showing details on click.
Been looking into ObservableObject, Binding, etc.
Tried #State in CrystalView but got lost pretty quickly.
What's the easiest way to pass data around views? Watched a few videos, still confused.
How do I pass crystals[key] into CrystalView()?
struct ContentView: View {
#State private var crystals = [String:Crystal]()
var body: some View {
Text("Crystals").font(.largeTitle)
NavigationView {
List {
ForEach(Array(crystals.keys), id:\.self) { key in
HStack {
NavigationLink(destination: CrystalView()) {
Text(key)
}
}
}
}.onAppear(perform:loadData)
}
}
func loadData() {
guard let url = URL(string: "https://lit-castle-74820.herokuapp.com/api/crystals") else { return }
URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data else { return }
do {
let decoded = try JSONDecoder().decode([String:Crystal].self, from: data)
DispatchQueue.main.async {
print(decoded)
self.crystals = decoded
}
} catch let jsonError as NSError {
print("JSON decode failed: \(jsonError)")
}
}.resume()
}
}
struct Crystal: Codable, Identifiable {
var id = UUID()
let composition, formation, colour: String
let metaphysical: [String]
}
struct CrystalView: View {
var body: some View {
Text("crystal")
}
}
try this approach, works well for me:
struct Crystal: Codable, Identifiable {
var id = UUID()
let composition, formation, colour: String
let metaphysical: [String]
// -- here, no `id`
enum CodingKeys: String, CodingKey {
case composition,formation,colour,metaphysical
}
}
struct CrystalView: View {
#State var crystal: Crystal? // <-- here
var body: some View {
Text("\(crystal?.composition ?? "no data")")
}
}
struct ContentView: View {
#State private var crystals = [String:Crystal]()
var body: some View {
Text("Crystals").font(.largeTitle)
NavigationView {
List {
ForEach(Array(crystals.keys), id:\.self) { key in
HStack {
NavigationLink(destination: CrystalView(crystal: crystals[key])) { // <-- here
Text(key)
}
}
}
}.onAppear(perform: loadData)
}
}
func loadData() {
guard let url = URL(string: "https://lit-castle-74820.herokuapp.com/api/crystals") else { return }
URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data else { return }
do {
let decoded = try JSONDecoder().decode([String:Crystal].self, from: data)
DispatchQueue.main.async {
print("decoded: \(decoded)")
self.crystals = decoded
}
} catch let jsonError as NSError {
print("JSON decode failed: \(jsonError)")
}
}.resume()
}
}
Something like this:
struct ContentView: View {
#State private var crystals: [Crystal] = []
var body: some View {
NavigationView {
List {
ForEach(crystals) { crystal in
NavigationLink(destination: CrystalView(crystal: crystal)) {
Text(crystal.name)
}
}
}
.navigationTitle("Crystals")
// initial detail view
Text("Select a crystal")
}
.task {
crystals = try? await fetchCrystals()
}
}
func fetchCrystals() async throws -> [Crystal] {
let (data, _) = try await URLSession.shared.data(from: "https://lit-castle-74820.herokuapp.com/api/crystals")
let decoder = JSONDecoder()
return try decoder.decode([Crystal].self, from: data) // you might want to convert this downloaded struct into a more suitable struct for the app.
}
}
struct CrystalView: View {
let crystal: Crystal
var body: some View {
Text(crystal.composition)
.navigationTitle(crystal.name)
}
}

#EnvironmentVariable not being passed to child views

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

How to use data from 1 tab view to another tab view in Swiftui? Looking for a approach

I have 2 tabs and the associated views are tabAView and tabBView.
On tabAView, 1 API call is there and got user object which is Published object in its ViewModel. ViewModel name is UserViewModel. UserViewModel is being observed by tabAView.
On tabBView, I have to use that user object. Because on some actions, user object value is changed, those changes should be reflected on subsequent views.
I am confused about the environment object usage here. Please suggest what will be the best approach.
Here is the code to understand better my problem.
struct ContentView: View {
enum AppPage: Int {
case TabA=0, TabB=1
}
#StateObject var settings = Settings()
var viewModel: UserViewModel
var body: some View {
NavigationView {
TabView(selection: $settings.tabItem) {
TabAView(viewModel: viewModel)
.tabItem {
Text("TabA")
}
.tag(AppPage.TabA)
AppsView()
.tabItem {
Text("Apps")
}
.tag(AppPage.TabB)
}
.accentColor(.white)
.edgesIgnoringSafeArea(.top)
.onAppear(perform: {
settings.tabItem = .TabA
})
.navigationBarTitleDisplayMode(.inline)
}
.environmentObject(settings)
}
}
This is TabAView:
struct TabAView: View {
#ObservedObject var viewModel: UserViewModel
#EnvironmentObject var settings: Settings
init(viewModel: UserViewModel) {
self.viewModel = viewModel
}
var body: some View {
Vstack {
/// code
}
.onAppear(perform: {
/// code
})
.environmentObject(settings)
}
}
This is the UserViewModel where API is hit and user object comes:
class UserViewModel: ObservableObject {
private var apiService = APIService.shared
#Published var user: EndUserData?
init () {
getUserProfile()
}
func getUserProfile() {
apiService.getUserAccount() { user in
DispatchQueue.main.async {
self.user = user
}
}
}
}
Below is the APIService function, where the user object is saved into UserDefaults for use. Which I know is incorrect.(That is why I am looking for another solution). Hiding the URL, because of its confidential.
func getUserAccount(completion: #escaping (EndUserData?) -> Void) {
self.apiManager.makeRequest(toURL: url, withHttpMethod: .get) { results in
guard let response = results.response else { return completion(nil) }
if response.httpStatusCode == 200 {
guard let data = results.data else { return completion(nil) }
do {
let str = String(decoding: data, as: UTF8.self)
print(str)
let decoder = JSONDecoder()
let responseData = try decoder.decode(ResponseData<EndUserData>.self, from: data)
UserDefaults.standard.set(data, forKey: "Account")
completion(responseData.data)
} catch let jsonError as NSError {
print(jsonError.localizedDescription)
return completion(nil)
}
}
}
}
This is another TabBView:
struct TabBView: View {
var user: EndUserData?
init() {
do {
guard let data = UserDefaults.standard.data(forKey: "Account") else {
return
}
let decoder = JSONDecoder()
let responseData = try decoder.decode(ResponseData<EndUserData>.self, from: data)
user = responseData.data
} catch let jsonError as NSError {
print(jsonError.localizedDescription)
}
}
var body: some View {
VStack (spacing: 10) {
UserSearch()
}
}
}
This is another view in TabBView, where the User object is used. Changes are not reflecting here.
struct UserSearch: View {
private var user: EndUserData?
init(comingFromAppsSection: Bool) {
do {
guard let data = UserDefaults.standard.data(forKey: "Account") else {
return
}
let decoder = JSONDecoder()
let responseData = try decoder.decode(ResponseData<EndUserData>.self, from: data)
user = responseData.data
} catch let jsonError as NSError {
print(jsonError.localizedDescription)
}
}
var body: some View {
Vstack {
Text(user.status)
}
}
}
I have removed most of the code from a confidential point of view but this code will explain the reason and error. Please look into the code and help me.

Calling a new view in a function SwiftUI

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])
}
}
}
}
}

Why doesn't this SwiftUI View update when changing a #State variable?

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.