This question already has answers here:
What is the difference between ObservedObject and StateObject in SwiftUI
(6 answers)
Saving Date/Time to UserDefaults through didSet on #Published var (from TimePicker component of SwiftUI)
(1 answer)
Closed 2 years ago.
i'm new to swiftUI and trying to create simple app for showing images from API of images
i got the images from url but can't show it in Imageview ,i have button and when i press it , it calls the api and return single image url and i don't figure what the problem is
import SwiftUI
struct ContentView: View {
#ObservedObject var NetworkManager = Network()
var body: some View {
ZStack{
VStack{
ImageDog(UIDogImage:self.NetworkManager.Dog ?? UIImage(systemName: "photo")!)
Button(action: {
self.NetworkManager.ApiCaller()
}) {
Text("GET DOG").font(.system(size: 20)).foregroundColor(Color(#colorLiteral(red: 0.5272222161, green: 0.6115953326, blue: 0.6786056161, alpha: 1))).padding()
}.background(Color(#colorLiteral(red: 0.5401973128, green: 0.9296894073, blue: 0.6209766269, alpha: 1))).cornerRadius(20)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ImageDog: View {
#State var UIDogImage:UIImage
var body: some View {
Image(uiImage:UIDogImage).resizable().frame(width: 200, height: 200, alignment: .center).padding()
}
}
and the Network caller where the image url returned and i change it to image then pass it to Dog property image
import SwiftUI
class Network:ObservableObject {
#Published var Dog:UIImage?
// This Api return random dog images every time you request it
// https://dog.ceo/api/breeds/image/random
// MARK: Network Caller
func ApiCaller(){ // used URLSession for Network requesting and Codable For parsing JSON
// 1 :: create URL
if let DogURL = URL(string: "https://dog.ceo/api/breeds/image/random") {
// 2 :: create URLSession
let session = URLSession(configuration: .default)
// 3 :: give Sesion Task
let Dogtask = session.dataTask(with: DogURL) { (data, response, error) in
if error == nil {
if let DogData = data {
do {
let DogResponseResult = try JSONDecoder().decode(DogResponse.self, from: DogData)
print(DogResponseResult.message!)
if let data = try? Data(contentsOf: URL(string: DogResponseResult.message!)!){
DispatchQueue.main.async {
self.Dog = UIImage(data: data)
}
}
}catch{
print(error)
}
}
}
}
// 4 :: Start Session Task
Dogtask.resume() // Send the request here
}
}
}
Related
How can I make a slider when my Data is coming from API? I am using
this(below code) for static images work fine but whenever I try to
use API data then my code does not work.
How to Set the Marquee in this images.
This is My code
public struct MagazineModel: Decodable {
public let magzineBanners: [MagzineBanner]
}
public struct MagzineBanner: Decodable, Identifiable {
public let id: Int
public let url: String
}
This is My View Model
//View Model for Magazines and showing Details
class MagazineBannerVM: ObservableObject{
#Published var datas = [MagzineBanner]()
let url = "ApiUrl"
init() {
getData(url: url)
}
func getData(url: String) {
guard let url = URL(string: url) else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
if let data = data {
do {
let results = try JSONDecoder().decode(MagazineModel.self, from: data)
DispatchQueue.main.async {
self.datas = results.magzineBanners
}
}
catch {
print(error)
}
}
}.resume()
}
}
struct MagazineBannerView: View{
#ObservedObject var list = MagazineBannerVM()
public let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
#State var currentIndex = 0
#State var totalImages = 2
var body: some View{
ScrollView(.horizontal) {
GeometryReader { proxy in
TabView(selection: $currentIndex) {
HStack{
ForEach(list.datas, id: \.id){ item in
Group{
AsyncImage(url: URL(string: item.url)){ image in
image
.resizable()
.frame(width:UIScreen.main.bounds.width, height: 122)
}placeholder: {
Image("logo_gray").resizable()
.frame(width:UIScreen.main.bounds.width, height: 122)
}
}
}
}
}
.tabViewStyle(PageTabViewStyle())
.onReceive(timer, perform: { _ in
withAnimation{
currentIndex = currentIndex < totalImages ? currentIndex + 1: 0
}
})
}
}
}
}
I want to change images after every 2 seconds and every images has
full width as the screen width
And it is showing the half of screen width and showing both images in
single view
I am trying to create a view that displays results from an API call, however I keep on running into multiple errors.
My question is basically where is the best place to make such an API call.
Right now I am "trying" to load the data in the "init" method of the view like below.
struct LandingView: View {
#StateObject var viewRouter: ViewRouter
#State var user1: User
#State var products: [Product] = []
init(_ viewRouter : ViewRouter, user: User) {
self.user1 = user
_viewRouter = StateObject(wrappedValue: viewRouter)
ProductAPI().getAllProducts { productArr in
self.products = productArr
}
}
var body: some View {
tabViewUnique(prodArrParam: products)
}
}
I keep on getting an "escaping closure mutating self" error, and while I could reconfigure the code to stop the error,I am sure that there is a better way of doing what I want.
Thanks
struct ContentView: View {
#State var results = [TaskEntry]()
var body: some View {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.title)
}
// this one onAppear you can use it
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/todos") else {
print("Your API end point is Invalid")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let response = try? JSONDecoder().decode([TaskEntry].self, from: data) {
DispatchQueue.main.async {
self.results = response
}
return
}
}
}.resume()
}
}
In .onAppear you can make api calls
I'm trying to set up the #AppStorage wrapper in my project.
I'm pulling Texts from a JSON API (see DataModel), and am hoping to store the results in UserDefautls. I want the data to be fetched .OnAppear and stored into the #AppStorage. When the user taps "Get Next Text", I want a new poem to be fetched, and to update #AppStorage with the newest Text data, (which would delete the past Poem stored).
Currently, the code below builds but does not display anything in the Text(currentPoemTitle).
Data Model
import Foundation
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
public class FetchPoem: ObservableObject {
// 1.
#Published var poems = [Poem]()
init() {
getPoem()
}
func getPoem() {
let url = URL(string: "https://poetrydb.org/random/1")!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let poemData = data {
// 3.
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
TestView
import SwiftUI
struct Test: View {
#ObservedObject var fetch = FetchPoem()
#AppStorage("currentPoemtTitle") var currentPoemTitle = ""
#AppStorage("currentPoemAuthor") var currentPoemAuthor = ""
var body: some View {
VStack{
Text(currentPoemTitle)
Button("Fetch next text") {
fetch.getPoem()
}
}.onAppear{
if let poem = fetch.poems.first {
currentPoemTitle = "\(poem.title)"
currentPoemAuthor = "\(poem.author)"
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
What am I missing? Thanks.
Here are a few code edits to get you going.
I added AppStorageKeys to manage the #AppStorage keys, to avoid errors retyping key strings (ie. "currentPoemtTitle")
Your question asked how to update the #AppStorage with the data, and the simple solution is to add the #AppStorage variables within the FetchPoem class and set them within the FetchPoem class after the data is downloaded. This also avoids the need for the .onAppear function.
The purpose of using #ObservedObject is to be able to keep your View in sync with the data. By adding the extra layer of #AppStorage, you make the #ObservedObject sort of pointless. Within the View, I added a Text() to display the title using the #ObservedObject values directly, instead of relying on #AppStorage. I'm not sure if you want this, but it would remove the need for the #AppStorage variables entirely.
I also added a getPoems2() function using Combine, which is a new framework from Apple to download async data. It makes the code a little easier/more efficient... getPoems() and getPoems2() both work and do the same thing :)
Code:
import Foundation
import SwiftUI
import Combine
struct AppStorageKeys {
static let poemTitle = "current_poem_title"
static let poemAuthor = "current_poem_author"
}
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
public class FetchPoem: ObservableObject {
#Published var poems = [Poem]()
#AppStorage(AppStorageKeys.poemTitle) var poemTitle = ""
#AppStorage(AppStorageKeys.poemAuthor) var poemAuthor = ""
init() {
getPoem2()
}
func getPoem() {
let url = URL(string: "https://poetrydb.org/random/1")!
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
guard let poemData = data else {
print("No data")
return
}
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
self.updateFirstPoem()
}
} catch {
print("Error")
}
}
.resume()
}
func getPoem2() {
let url = URL(string: "https://poetrydb.org/random/1")!
URLSession.shared.dataTaskPublisher(for: url)
// fetch on background thread
.subscribe(on: DispatchQueue.global(qos: .background))
// recieve response on main thread
.receive(on: DispatchQueue.main)
// ensure there is data
.tryMap { (data, response) in
guard
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return data
}
// decode JSON data to [Poem]
.decode(type: [Poem].self, decoder: JSONDecoder())
// Handle results
.sink { (result) in
// will return success or failure
print("poetry fetch completion: \(result)")
} receiveValue: { (value) in
// if success, will return [Poem]
// here you can update your view
self.poems = value
self.updateFirstPoem()
}
// After recieving response, the URLSession is no longer needed & we can cancel the publisher
.cancel()
}
func updateFirstPoem() {
if let firstPoem = self.poems.first {
self.poemTitle = firstPoem.title
self.poemAuthor = firstPoem.author
}
}
}
struct Test: View {
#ObservedObject var fetch = FetchPoem()
#AppStorage(AppStorageKeys.poemTitle) var currentPoemTitle = ""
#AppStorage(AppStorageKeys.poemAuthor) var currentPoemAuthor = ""
var body: some View {
VStack(spacing: 10){
Text("App Storage:")
Text(currentPoemTitle)
Text(currentPoemAuthor)
Divider()
Text("Observed Object:")
Text(fetch.poems.first?.title ?? "")
Text(fetch.poems.first?.author ?? "")
Button("Fetch next text") {
fetch.getPoem()
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
As an example I have a SwitUI ContentView. The one that comes when you first make the project.
import SwiftUI
struct ContentView: View {
var manager = TestManager()
var body: some View {
ZStack{
Color(.green)
.edgesIgnoringSafeArea(.all)
VStack {
Text("Test Text")
Button(action:{}) {
Text("Get number 2")
.font(.title)
.foregroundColor(.white)
.padding()
.overlay(RoundedRectangle(cornerRadius: 30)
.stroke(Color.white, lineWidth: 5))
}
}
}
}
}
I have a TestManager that will handle an Api call. I Made a delegate for the class that has two functions.
protocol TestManagerDelegate {
func didCorrectlyComplete(_ testName: TestManager, model: TestModel)
func didFailWithError(_ error: Error)
}
struct TestManager {
var delegate: TestManagerDelegate?
let urlString = "http://numbersapi.com/2/trivia?json"
func Get(){
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error != nil{
self.delegate?.didFailWithError(error!)
return
}
if let safeData = data{
if let parsedData = self.parseJson(safeData){
self.delegate?.didCorrectlyComplete(self, model: parsedData)
}
}
}
task.resume()
}
}
func parseJson(_ jsonData: Data) -> TestModel?{
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(TestModel.self, from: jsonData)
let mes = decodedData.message
let model = TestModel(message: mes)
return model
} catch {
delegate?.didFailWithError(error)
return nil
}
}
}
This is the testModel data class. Only grabbing the text of the Json returned.
struct TestModel :Decodable{
let text: String
}
How do I connect the TestManager to the view and have the view handle the delegate like how we could do in in storyboards?
Regarding the TestModel
Decodable protocol (in your context) assumes you to create the model struct with all the properties, that you get via JSON. When requesting http://numbersapi.com/2/trivia?json you'll get something like:
{
"text": "2 is the number of stars in a binary star system (a stellar system consisting of two stars orbiting around their center of mass).",
"number": 2,
"found": true,
"type": "trivia"
}
Which means, your model should look like the following:
struct TestModel: Decodable {
let text: String
let number: Int
let found: Bool
let type: String
}
Regarding Delegates
In SwiftUI this approach is not reachable. Instead, developers need to adapt the Combine framework's features: property wrappers #ObservedObject, #Published, and ObservableObject protocol.
You want to put your logic into some struct. Bad news, that (currently) ObservableObject is AnyObject protocol (i.e. Class-Only Protocol). You'll need to rewrite your TestManager as class as:
class TestManager: ObservableObject {
// ...
}
Only then you could use it in your CurrentView using #ObservedObject property wrapper:
struct ContentView: View {
#ObservedObject var manager = TestManager()
// ...
}
Regarding the TestManager
Your logic now excludes the delegate as such, and you need to use your TestModel to pass the data to your CustomView. You could modify TestManager by adding new property with #Published property wrapper:
class TestManager: ObservableObject {
let urlString = "http://numbersapi.com/2/trivia?json"
// 1
#Published var model: TestModel?
func get(){
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { [weak self] (data, response, error) in
// 2
DispatchQueue.main.async {
if let safeData = data {
if let parsedData = self?.parseJson(safeData) {
// 3
self?.model = parsedData
}
}
}
}
task.resume()
}
}
private func parseJson(_ jsonData: Data) -> TestModel? {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(TestModel.self, from: jsonData)
return decodedData
} catch {
return nil
}
}
}
To be able to access your model "from outside", in your case the ContentView.
Use DispatchQueue.main.async{ } for async tasks, because Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
Simply use your parsed model.
Then in ContentView use your TestManager like this:
struct ContentView: View {
#ObservedObject var manager = TestManager()
var body: some View {
ZStack{
Color(.green)
.edgesIgnoringSafeArea(.all)
VStack {
Text("Trivia is: \(self.manager.model?.text ?? "Unknown")")
Button(action:{ self.manager.get() }) {
Text("Get number 2")
.font(.title)
.foregroundColor(.white)
.padding()
.overlay(RoundedRectangle(cornerRadius: 30)
.stroke(Color.white, lineWidth: 5))
}
}
}
}
}
Regarding HTTP
You use the link http://numbersapi.com/2/trivia?json which is not allowed by Apple, please, use https instead, or add the App Transport Security Settings key with Allow Arbitrary Loads parameter set to YES into your Info.Plist. But do this very carefully as the http link simply will not work.
Further steps
You could implement the error handling by yourself basing on the description above.
Full code (copy-paste and go):
import SwiftUI
struct ContentView: View {
#ObservedObject var manager = TestManager()
var body: some View {
ZStack{
Color(.green)
.edgesIgnoringSafeArea(.all)
VStack {
Text("Trivia is: \(self.manager.model?.text ?? "Unknown")")
Button(action:{ self.manager.get() }) {
Text("Get number 2")
.font(.title)
.foregroundColor(.white)
.padding()
.overlay(RoundedRectangle(cornerRadius: 30)
.stroke(Color.white, lineWidth: 5))
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class TestManager: ObservableObject {
let urlString = "http://numbersapi.com/2/trivia?json"
#Published var model: TestModel?
func get(){
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { [weak self] (data, response, error) in
DispatchQueue.main.async {
if let safeData = data {
if let parsedData = self?.parseJson(safeData) {
self?.model = parsedData
}
}
}
}
task.resume()
}
}
private func parseJson(_ jsonData: Data) -> TestModel? {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(TestModel.self, from: jsonData)
return decodedData
} catch {
return nil
}
}
}
struct TestModel: Decodable {
let text: String
let number: Int
let found: Bool
let type: String
}
I'm trying to display images from a list of objects stored in Firebase. Initially the image loads fine, but if I switch to a different view and return to the list view the image never loads again.
Gif of the described bug
The image data seems to be saved as expected on both load attempts:
here
Below is my code for the image loader, which uses a url to fetch the images from Firebase Storage, and the list row that contains the image.
ImageLoader.swift
import Foundation
import SwiftUI
import Firebase
import FirebaseFirestore
class ImageLoader: ObservableObject {
#Published var dataIsValid = false
var data:Data?
func loadImage(url: String) {
let imageRef = Storage.storage().reference(forURL: url)
imageRef.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print("\(error)")
}
guard let data = data else { return }
DispatchQueue.main.async {
print(self.dataIsValid)
self.dataIsValid = true
self.data = data
}
}
}
func imageFromData() -> UIImage {
UIImage(data: self.data!)!
}
}
ListRow.swift
import SwiftUI
import Combine
struct EventRow: View {
#ObservedObject var imageLoader = ImageLoader()
var imageUrl: String
var body: some View {
HStack {
Image(uiImage: self.imageLoader.dataIsValid ? self.imageLoader.imageFromData() : UIImage())
.resizable()
.frame(width: 100.0, height: 140.0)
.background(Color.gray)
.clipShape(RoundedRectangle(cornerRadius: 5.0))
}
.onAppear {
self.imageLoader.loadImage(url: self.imageUrl)
}
}
}
The way I fixed this was by creating a custom ImageView and handling the image loading within this view. I figured this out by following this tutorial and realized that was the step I was missed. If anyone can explain why using the built-in SwiftUI Image() causes this issue I would really appreciate it.
ListRow.swift
import SwiftUI
struct ListRow: View {
var imageUrl: String
var body: some View {
HStack {
FBURLImage(url: imageUrl)
}
}
}
FBURLImage.swift
import SwiftUI
struct FBURLImage: View {
#ObservedObject var imageLoader: ImageLoader
init(url: String) {
imageLoader = ImageLoader()
imageLoader.loadImage(url: url)
}
var body: some View {
Image(uiImage:
imageLoader.data != nil ? UIImage(data: imageLoader.data!)! : UIImage())
.resizable()
.frame(width: 100.0, height: 140.0)
.background(Color.gray)
.clipShape(RoundedRectangle(cornerRadius: 5.0))
}
}
ImageLoader.swift
import Foundation
import SwiftUI
import Firebase
import FirebaseFirestore
class ImageLoader: ObservableObject {
#Published var data: Data?
func loadImage(url: String) {
let imageRef = Storage.storage().reference(forURL: url)
imageRef.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print("\(error)")
}
guard let data = data else { return }
DispatchQueue.main.async {
self.data = data
}
}
}
}