Updating swiftui text view after parsing json data - ios

I have a function that goes out to an api and gets a stock price. I have the function returning a double and I can see the correct price print out to the console when I run the code. When I try to set that price to a variable inside the function so the function can return it, I just get 0.000 in the text view. Can someone tell me what I'm doing wrong? My code is below.
import SwiftUI
struct ListView: View {
var body: some View {
List {
HStack {
Text("Stock Price (15 Min. delay)")
Spacer()
Text("\(getStockPrice(stock: symbol))")
}
}
}
func getStockPrice(stock: String) -> Double {
var thePrice = Double()
guard let url = URL(string: "my url to get data") else {
fatalError("URL does not work!")
}
URLSession.shared.dataTask(with: url) { jsonData, _, _ in
guard let jData = jsonData else {return}
do {
if let json = try JSONSerialization.jsonObject(with: jData, options: []) as? [String: Any] {
if let pricer = json["latestPrice"] as? Double {
print(pricer)
thePrice = pricer
}
}
} catch let err {
print(err.localizedDescription)
}
}.resume()
return thePrice
}
}

You can update your UI with changing the #State variable, like in code below:
struct UpdatingPriceAsync: View {
#State var stockPrice: Double = 0.0
var body: some View {
List {
HStack {
Text("Stock Price (15 Min. delay)")
Spacer()
Text("\(stockPrice)")
.onAppear() {
self.updateStockPrice(stock: "something")
}
}
}
}
private func updateStockPrice(stock: String) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // sort of URL session task
DispatchQueue.main.async { // you need to update it in main thread!
self.stockPrice = 99.9
}
}
}
}

Related

SwiftUI Navigation - List loading multiple time after navigating from details

I am creating a SwiftUI List with Details.
This list is fetching JSON data from Firebase Realtime. The data consist of 5 birds with an ID, a name and an image URL.
My problem is the following:
Each time I click on the back button after I navigate to details, the data get doubled every single time, what am I doing wrong? (see screenshots).
I am using MVVM design pattern, I am listening and removing that listener every time the View appears and disappears.
Please, find the code below:
Main View:
var body: some View {
NavigationStack {
List(viewModel.birds) { bird in
NavigationLink(destination: DetailsView(bird: bird)) {
HStack {
VStack(alignment: .leading) {
Text(bird.name).font(.title3).bold()
}
Spacer()
AsyncImage(url: URL(string: bird.imageURL)) { phase in
switch phase {
// downloading image here
}
}
}
}
}.onAppear {
viewModel.listentoRealtimeDatabase()
}
.onDisappear {
viewModel.stopListening()
}.navigationTitle("Birds")
}
}
DetailsView:
struct DetailsView: View {
var bird: Bird
var body: some View {
Text("\(bird.name)")
}
}
Model:
struct Bird: Identifiable, Codable {
var id: String
var name: String
var imageURL: String
}
View Model:
final class BirdViewModel: ObservableObject {
#Published var birds: [Bird] = []
private lazy var databasePath: DatabaseReference? = {
let ref = Database.database().reference().child("birds")
return ref
}()
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
func listentoRealtimeDatabase() {
guard let databasePath = databasePath else {
return
}
databasePath
.observe(.childAdded) { [weak self] snapshot in
guard
let self = self,
var json = snapshot.value as? [String: Any]
else {
return
}
json["id"] = snapshot.key
do {
let birdData = try JSONSerialization.data(withJSONObject: json)
let bird = try self.decoder.decode(Bird.self, from: birdData)
self.birds.append(bird)
} catch {
print("an error occurred", error)
}
}
}
func stopListening() {
databasePath?.removeAllObservers()
}
}
screenshot how it should be

SwiftUI what is the return-type of my own render's function?

can i define something like func myRender() -> Image | Text (union-types) ?
hi, all. i think i miss a unsolvable problem on my SwiftUI demo: a simple url image renderer with a sub-render piece, codes here:
import Foundation
import SwiftUI
struct URLImage: View {
var urlString: String;
#State var loaded = false
#State var error: Any?
#State var data: Data?
func didMount() {
print("!!! viewWillAppear", self.urlString)
// just load url and get data in the callback fn:
// (String, (Any?, Data) -> Void) -> Void
// $0 is error, $1 is data
loadImage(urlString: self.urlString) {
print("loaded", $0 ?? "no error", $1)
self.error = $0
self.data = $1
self.loaded = true
}
}
// what is the return-type of tryRender ?
func tryRender() -> View { // doesn't works now because `View` is a protocol
// self.data maybe nil or with a wrong binary format
guard let image = try? Image(nsImage: NSImage(data: self.data!)!) else {
// return Image("image_format_error") // it works with "func tryRender() -> Image" but ... emmm
return Text("image_format_error") // this is what i want, but this can't works
}
return image
}
var body: some View {
self.didMount()
return VStack {
if (self.error != nil) {
Text("URLImage Error")
} else {
if (self.loaded) {
self.tryRender()
} else {
Text("not loaded")
}
}
}
}
}
struct URLImage_Previews: PreviewProvider {
static var previews: some View {
URLImage(
urlString: "http://127.0.0.1:3000/0009.jpg"
)
}
}
the problem is the return type of tryRender(), i can't write tryRender() -> Image | Text like typescript's union types to express my opinion.
is Image or Text have a base type to make it works ? or there is another way to write such the sub-render pieces func ?
You can use #ViewBuilder and your return type should be some View
#ViewBuilder func tryRender() -> some View {
if let image = try? Image(nsImage: NSImage(data: self.data!)!) {
image
} else {
Text("image_format_error")
}
}

How to highlight text of offline JSON data that are shown in SwiftUI's Disclosure group/ or Detail view?

What we have:
We are currently passing the data of book(json file saved in document directory) shown in swiftUI's Disclosure group. old book data is stored in core-data and shown in UIKit's WKWebView.
Scenario:
Goal:
option 1: We want to implement the Search function that can search specific words and highlight it and then show in the small scroll view which can navigate the user to specific place in the shown data.
Below is the Search option that has been implemented in core-data that can search the specific word or term that lets user navigate to specific page. But, unfortunately it is in UIKit and is for old books(core-data) only. While, I want to implement such feature for SwiftUI.
Option 2: We want to implement the search function in the final detail view of the SwiftUI chapters where the stories(data)(attributed/html text) of the book can be read. Here the user should be able to search specific words in various languages. Now the words should be highlighted and can user is able to navigate to the exact location of the highlighted word in the page along with the total words that match the search and page count.
Below is the detail view where the search can be implemented.
What i have tried:
I have implemented the demo SwiftUI search
** Code:**
Below is the code where the Book structure:
import Foundation
import SwiftUI
import UIKit
struct ContentView: View {
#EnvironmentObject var booksList: BooksList
#State var books: [BookModel] = []
#State var selection: BookModel?
var body: some View {
// NavigationView {
VStack(alignment: .trailing, spacing: 40) {
ScrollView(.vertical, showsIndicators: false) {
ForEach(booksList.books){ book in
//Alternative way , each book on seperate view
//NavigationLink(destination: lvl4(books: [book], selection: nil)){
// Text(book.bukTitle!)
//
if #available(iOS 15.0, *) {
if #available(iOS 15, *){
// label: do {
//
// }
// label: do {
// AsyncImage(url: URL(string: "\(book.coverImage!)")) .scaledToFit() .fixedSize(horizontal:true, vertical: true) .frame(minWidth: 10, maxWidth: 20)
// }
}
DisclosureGroup( "\(Text(book.bukTitle!) .fontWeight(.medium) .font(.system(size: 30)))"
) {
ForEach(book.bookContent ?? []) { bookContent in
DisclosureGroup(
"\(Text(bookContent.title).fontWeight(.medium) .font(.system(size: 25)))"
) {
OutlineGroup(bookContent.child, children: \.child) { item in
NavigationLink {
if #available(iOS 15, *) {
ScrollView {
Text(attributedString(from: item.title, font: Font.system(size: 25)))
.padding(30).lineSpacing(20).navigationTitle(Text(bookContent.title))
.navigationBarTitleDisplayMode(.inline)
}.frame(minWidth: 0, maxWidth: .infinity)
.background(Color(UIColor.hexStringToUIColor(hexStr: ("efe8d2"))))
}
} label: {
if #available(iOS 15, *) {
Text(
"\(Text(attributedString(from: item.title, font: Font.system(size: 23) )))"
)
.lineLimit(2).lineSpacing(15) .padding(15)
} else {
// Fallback on earlier versions
}
}
.lineSpacing(15) .disabled(item.child != nil)
}
.lineSpacing(20) .padding() }
}.lineSpacing(20) .padding()
}.overlay {
RoundedRectangle(cornerRadius: 10)
.stroke(lineWidth: 2)
}
}
}
}
}.padding(10)
}
VBBookManager.Swift including Book Model code::
//
// VBBooksManager.swift
// VadtaldhamBooks
//
import Foundation
enum BookParseError: Error {
case bookParsingFailed
}
struct BookModelForJSONConversion: Codable {
var id:Int
var title: String?
var content: [BookContent]?
var bookCoverImage:String?
func convertToJsonString()->String?{
let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted
var encodedString:String?
do {
let encodePerson = try jsonEncoder.encode(self)
let endcodeStringPerson = String(data: encodePerson, encoding: .utf8)!
//print(endcodeStringPerson)
encodedString = endcodeStringPerson
} catch {
print(error.localizedDescription)
return nil
}
return encodedString
}
}
struct BookModel: Identifiable, Codable {
var id:Int
var bukTitle: String?
var isLive: Bool?
var userCanCopy: Bool?
var bookContent: [BookContent]?
var coverImage:String?
enum CodingKeys: String, CodingKey {
case id = "id"
case bukTitle = "title"
case isLive = "is_live"
case userCanCopy = "user_can_copy"
case bookContent = "content"
case coverImage = "bookCoverImage"
}
}
struct BookContent: Identifiable, Codable {
let id = UUID()
var title, type: String
var child: [Child]
}
struct Child: Identifiable, Codable {
let id = UUID()
var title, type: String
var child: [Child]?
}
enum BooksDirectory {
/// Default, system Documents directory, for persisting media files for upload.
case downloads
/// Returns the directory URL for the directory type.
///
fileprivate var url: URL {
let fileManager = FileManager.default
// Get a parent directory, based on the type.
let parentDirectory: URL
switch self {
case .downloads:
parentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
}
return parentDirectory.appendingPathComponent(VBBooksManager.booksDirectoryName, isDirectory: true)
}
}
class VBBooksManager:NSObject {
fileprivate static let booksDirectoryName = "books"
let directory: BooksDirectory
#objc (defaultManager)
static let `default`: VBBooksManager = {
return VBBooksManager()
}()
// MARK: - Init
/// Init with default directory of .uploads.
///
/// - Note: This is particularly because the original Media directory was in the NSFileManager's documents directory.
/// We shouldn't change this default directory lightly as older versions of the app may rely on Media files being in
/// the documents directory for upload.
///
init(directory: BooksDirectory = .downloads) {
self.directory = directory
}
// MARK: - Instance methods
/// Returns filesystem URL for the local Media directory.
///
#objc func directoryURL() throws -> URL {
let fileManager = FileManager.default
let mediaDirectory = directory.url
// Check whether or not the file path exists for the Media directory.
// If the filepath does not exist, or if the filepath does exist but it is not a directory, try creating the directory.
// Note: This way, if unexpectedly a file exists but it is not a dir, an error will throw when trying to create the dir.
var isDirectory: ObjCBool = false
if fileManager.fileExists(atPath: mediaDirectory.path, isDirectory: &isDirectory) == false || isDirectory.boolValue == false {
try fileManager.createDirectory(at: mediaDirectory, withIntermediateDirectories: true, attributes: nil)
}
return mediaDirectory
}
func saveBook(bookName:String,bookData:String)->Error?{
//TODO: Save book into Document directory
do {
var finalBookName = bookName
if !finalBookName.contains(".json"){
finalBookName = "\(bookName).json"
}
let bookPath = try? self.directoryURL().appendingPathComponent(finalBookName)
print(bookPath?.relativePath)
do {
let fileManager = FileManager.default
if fileManager.fileExists(atPath: bookPath!.relativePath){
try fileManager.removeItem(at: bookPath!)
}
let data = Data(bookData.utf8)
try? data.write(to: bookPath!, options: .atomic)
//Just for Testing purpose call load book
// let bookModel = try loadBookFromDocumentDirectory(bookName: finalBookName)
// print(bookModel?.coverImage)
}
catch let error as NSError {
print(error)
return error
}
}
catch let error as NSError{
print(error)
return error
}
return nil
//fileManager.wri wr(bookPath.relativePath, contents: Data(bookData), attributes: nil)
}
//https://stackoverflow.com/questions/39415249/best-practice-for-swift-methods-that-can-return-or-error
func loadBookFromDocumentDirectory(bookName:String) throws -> BookModel? {
let fileManager = FileManager.default
do {
var finalBookName = bookName
if !finalBookName.contains(".json"){
finalBookName = "\(bookName).json"
}
let bookPath = try? self.directoryURL().appendingPathComponent(finalBookName)
print(bookPath?.relativePath)
do {
if fileManager.fileExists(atPath: bookPath!.relativePath){
let jsonBookString = fileManager.contents(atPath: bookPath!.relativePath)
do {
let data = try Data(jsonBookString!)
guard let parsedBookObject:BookModel? = try JSONDecoder().decode(BookModel.self, from: data) else {
throw BookParseError.bookParsingFailed
}
return parsedBookObject ?? nil
//print(parsedBookObject)
}
catch let error as NSError{
print("error: \(error)")
throw error
}
}else{
}
}
catch let error as NSError {
print(error)
throw error
}
}
catch let error as NSError{
print(error)
throw error
}
return nil
}
func loadAllSavedBooks()->[BookModel]?{
var allBooks:[BookModel] = []
let fileManager = FileManager.default
guard let booksPath = try? self.directoryURL() else {
return []
}
print(booksPath)
do {
// Get the directory contents urls (including subfolders urls)
let directoryContents = try fileManager.contentsOfDirectory(at: booksPath, includingPropertiesForKeys: nil)
print(directoryContents)
// if you want to filter the directory contents you can do like this:
let books = directoryContents.filter{ $0.pathExtension == "json" }
let bookNames = books.map{ $0.deletingPathExtension().lastPathComponent }
print("bookNames list:", bookNames)
//TODO: Load all the books and send array back
for bookName in bookNames {
do {
let book = try loadBookFromDocumentDirectory(bookName:bookName)
allBooks.append(book!)
} catch BookParseError.bookParsingFailed {
continue
}
}
return allBooks
} catch let error as NSError {
print(error)
}
return allBooks
}
func isBookAlreadyExists (bookName:String)->Bool {
return try! (loadBookFromDocumentDirectory(bookName: bookName) != nil)
}
func deleteBook (bookName:String)->Bool {
// guard let book = try! loadBookFromDocumentDirectory(bookName: bookName) else {
// return false
// }
do {
var finalBookName = bookName
if !finalBookName.contains(".json"){
finalBookName = "\(bookName).json"
}
let bookPath = try? self.directoryURL().appendingPathComponent(finalBookName)
// print(bookPath?.relativePath)
do {
let fileManager = FileManager.default
if fileManager.fileExists(atPath: bookPath!.relativePath){
try fileManager.removeItem(at: bookPath!)
return true
}
}
catch let error as NSError {
print(error)
return false
}
}
catch let error as NSError{
print(error)
return false
}
return false
}
}

How to connect enum/switch with an API value for a specific case swiftUI

I have a currency converter calculator application, Where data is fetched through the API. I am trying to convert inputted data through the enum and switch but unfortunately, I am not sure what is the mistake. Please Can someone help me to understand what shall I do?
You may find my GitHub Link for a project below
https://github.com/Chokaaaa/CurMe
This is my Fetch file
import SwiftUI
class FetchData: ObservableObject {
#Published var coversionData: [Currency] = []
#Published var baseCode = "USD"
init() {
fetch()
}
func fetch() {
let url = "https://open.exchangerate-api.com/v6/latest?base=\(baseCode)"
let session = URLSession(configuration: .default)
session.dataTask(with: URL(string: url)!) { data, _, _ in
guard let JSONData = data else {return}
do {
let conversion = try JSONDecoder().decode(Conversion.self, from: JSONData)
DispatchQueue.main.async {
self.coversionData = conversion.rates.compactMap({ (key,value) -> Currency? in
return Currency(currencyName: key, currencyValue: value)
})
.filter({ Currency in
Currency.currencyName == self.filteredCurrency
})
}
}
catch {
print(error)
}
}
.resume()
}
func updateData(baseCode: String) {
self.baseCode = baseCode
self.coversionData.removeAll()
fetch()
}
}
Below you may find an enum where I use a switch with a return of input from the custom number pad (like a calculator) and multiplied on the dummy value. I think I need to fix something in the switch case. Please guys someone help me!! I am struggling.
Bellow, you may find a code for a enum/switch
import SwiftUI
struct CurrencyView: View {
#StateObject var viewModel = FetchData()
enum currencyChoice {
case Kazakhstan, Rubles, Usa
func image() -> Image {
switch self {
case .Kazakhstan: return Image("kz")
case .Rubles: return Image("rub")
case .Usa: return Image("usa")
}
}
func operation(_ input: Double) -> Double {
switch self {
case .Kazakhstan: return (input * 2)
case .Rubles: return (input * 3)
case .Usa: return (input * 5)
}
}
}
var function : currencyChoice
#Binding var state : CalculationState
var body: some View {
return function.image()
.resizable()
.scaledToFill()
.frame(width: 80, height: 80)
.cornerRadius(40)
.onTapGesture {
state.currentNumber = function.operation(state.currentNumber)
}
}
}
In your case, enum is not suggested with store property so better to avoid it or use the static property of enum.
Definitely it'll fix your issue.

SwiftUI: How to set UserDefaults first time view renders?

So I have this code, where I fetch a url from firestore and then append it to an array, which is then stored in userDefaults(temporarily).
In the view I basically just iterate over the array stored in userdefaults and display the images.
But the problem is, that I have to rerender the view before the images show.
How can i fix this?
struct PostedImagesView: View {
#State var imagesUrls : [String] = []
#ObservedObject var postedImagesUrls = ProfileImages()
var body: some View {
VStack{
ScrollView{
ForEach(postedImagesUrls.postedImagesUrl, id: \.self) { url in
ImageWithURL(url)
}
}
}
.onAppear{
GetImage()
print("RAN GETIMAGE()")
}
}
// Get Img Url from Cloud Firestore
func GetImage() {
guard let userID = Auth.auth().currentUser?.uid else { return }
let db = Firestore.firestore()
db.collection("Users").document(userID).collection("PostedImages").document("ImageTwoTester").getDocument { (document, error) in
if let document = document, document.exists {
// Extracts the value of the "Url" Field
let imageUrl = document.get("Url") as? String
UserDefaults.standard.set([], forKey: "postedImagesUrls")
imagesUrls.append(imageUrl!)
UserDefaults.standard.set(imagesUrls, forKey: "postedImagesUrls")
} else {
print(error!.localizedDescription)
}
}
}
}

Resources