swiftui Array contents updated inside function closure but does not persist in other views - ios

When fetching data from my database, I update an array from the json received inside the fetching function. While inside the function, the array stays updated, but when I try to access the array in a separate view, the array is empty.
the code that fetches the data and updates the "events" array looks like this:
import Foundation
import SwiftUI
let apiUrl = "http://localhost:5000/"
class DataFetcher: ObservableObject {
#Published var events: [eventdata] = []
func fetchEvents(){
events.removeAll()
let url = NSMutableURLRequest(url: NSURL(string: apiUrl)! as URL)
url.httpMethod = "GET"
URLSession.shared.dataTask(with: url as URLRequest) { data, response, err in
if let err = err {
print(err.localizedDescription)
return
}
guard let response = response as? HTTPURLResponse else { return }
if response.statusCode == 200 {
guard let data = data else { return }
DispatchQueue.main.async {
do{
self.events = try JSONDecoder().decode([eventdata].self, from: data)
// print(self.events.last?.address)
}catch let err {
print("Error: \(err)")
}
}
} else {
print("HTTPURLResponse code: \(response.statusCode)")
}
}.resume()
//print(self.events.last?.address)
}
}
The view that calls this function looks like this:
import Foundation
import SwiftUI
struct CreateEventButton: View {
#ObservedObject var request = DataFetcher()
#State private var isPresentedEvent = false
#State private var eventName: String = ""
#State private var eventDescription: String = ""
#State private var selectedStartTime = Date()
#State private var selectedEndTime = Date()
#State private var startTimeStamp: String = ""
#State private var endTimeStamp: String = ""
#Binding var annotationSelected: Bool
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .full
return formatter
}
func send(_ sender: Any) {
let request = NSMutableURLRequest(url: NSURL(string: "http://localhost:5000/")! as URL)
request.httpMethod = "POST"
self.startTimeStamp = "\(self.selectedStartTime)"
self.endTimeStamp = "\(self.selectedEndTime)"
self.startTimeStamp.removeLast(5)
self.endTimeStamp.removeLast(5)
let postString = "b=\(self.eventName)&c=\(self.eventDescription)&d=\(self.startTimeStamp)&e=\(self.endTimeStamp)"
request.httpBody = postString.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
if error != nil {
print("error=\(String(describing: error))")
return
}
print("response = \(String(describing: response))")
let responseString = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)
print("responseString = \(String(describing: responseString))")
}
task.resume()
self.eventName = ""
self.eventDescription = ""
self.selectedStartTime = Date()
self.selectedEndTime = Date()
}
var body: some View {
//UNCOMMENT THIS STUFF BELOW TO MAKE AN EVENT CREATION BUTTON IN THE SHEET INSTEAD OF JUST DIRECTING TO A SHEET WITH THE EVENT CREATION DETAILS
Button(action: {
self.isPresentedEvent.toggle() //trigger modal presentation
}, label: {
Text("Create Event").font(.system(size: 18)).foregroundColor(Color(.darkGray)).shadow(radius: 8)
}).padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.foregroundColor(.secondary)
.background(Color(.secondarySystemBackground))
.cornerRadius(50.0)
.sheet(isPresented: $isPresentedEvent, content:{
VStack{
TextField("Event Name", text: self.$eventName).padding()
TextField("Event Description", text: self.$eventDescription).padding()
Form {
DatePicker("When your event starts: ", selection: self.$selectedStartTime, in: Date()...)
}
Form {
DatePicker("When your event ends: ", selection: self.$selectedEndTime, in: Date()...)
}
HStack{
Button(action: {
self.isPresentedEvent.toggle()
self.annotationSelected = false
print("Start: \(self.selectedStartTime)")
print("End: \(self.selectedEndTime)")
self.send((Any).self)
}, label: {
Text("Create Event")
})
Button(action: {
self.isPresentedEvent.toggle()
self.request.fetchEvents()
print("yo yo \(self.request.events.last?.address)")
}, label: {
Text("Cancel")
})
}
Text("Create Event Button (Non Functional)").padding()
}
} )
}
}
Any insight on where I might be going wrong is much appreciated

Button(action: {
self.isPresentedEvent.toggle()
self.request.fetchEvents()
}, label: {
if self.request.events.count > 0 {
Text("hey we have requests!")
} else {
Text("Cancel")
}
})
woops posted the accidently before finishing. one sec.
I believe it's a timing issue. The view is rendered before your request finishes fetching your data. You can add an if statement like above to update your view once the data is fully fetched.

Related

SwiftUI JSON Decoder not Working (using async await)

I am creating a new version of an existing app and want to use the new async await
format for a web request. Placing a break at the JSONDecoder().decode line I see that I do have data -
but the decoding does not work. (The url and my key DO work in the old version)
Here's the JSON format of the web source (shortened - there are many more items in
a fuel_station):
{
"station_locator_url":"https://afdc.energy.gov/stations/",
"total_results":110,
"station_counts":{},
"fuel_stations":[
{
"access_code":"public",
"access_days_time":"24 hours daily; call 866-809-4869 for Clean Energy card",
"access_detail_code":"KEY_ALWAYS",
"cards_accepted":"CleanEnergy",
"date_last_confirmed":"2021-09-10",
}
]
}
I created the following models from the above:
enum CodingKeys: String, CodingKey {
case fuelStations = "fuel_stations"
case accessCode = "access_code"
case accessDaysTime = "access_days_time"
case accessDetailCode = "access_detail_code"
case cardsAccepted = "cards_accepted"
case dateLastConfirmed = "date_last_confirmed"
}
struct TopLevel: Codable {
let fuelStations: [FuelStation]
}
struct FuelStation: Codable {
let accessCode, accessDaysTime, accessDetailCode, cardsAccepted: String
let dateLastConfirmed: String
let id: String
}
I put a simplified version of the initial view in one file for testing:
struct SiteListView: View {
#State private var fuelStations: [FuelStation] = []
#State private var topLevel: TopLevel = TopLevel(fuelStations: [])
var body: some View {
NavigationView {
VStack {
List(fuelStations, id: \.id) { item in
VStack {
Text(item.accessCode)
Text(item.accessDaysTime)
}
}
}
.navigationTitle("Site List View")
.task {
await loadData()
}
}//nav
}
func loadData() async {
//I believe the DEMO_KEY in the url will allow limited retrievals
guard let url = URL(string: "https://developer.nrel.gov/api/alt-fuel-stations/v1.json?api_key=DEMO_KEY") else {
print("Invalid URL")
return
}
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard (response as? HTTPURLResponse)?.statusCode == 200 else { return }
print("response status code is 200")
if let decodedResponse = try? JSONDecoder().decode(TopLevel.self, from: data) {
topLevel = decodedResponse
print("in decoding: topLevel.fuelStations.count is \(topLevel.fuelStations.count)")
//I would iterate through topLevel here and add to the fuelStations
//array but I never get here
}
} catch {
print("Invalid Data")
}
}//load data
}//struct
Any guidance would be appreciated. Xcode 13.2.1 iOS 15.2
First you should remove ? from try? for the catch to work when there is a problem in decoding like this
func loadData() async {
//I believe the DEMO_KEY in the url will allow limited retrievals
guard let url = URL(string: "https://developer.nrel.gov/api/alt-fuel-stations/v1.json?api_key=DEMO_KEY") else {
print("Invalid URL")
return
}
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard (response as? HTTPURLResponse)?.statusCode == 200 else { return }
print("response status code is 200")
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let decodedResponse = try decoder.decode(TopLevel.self, from: data)
print("in decoding: topLevel.fuelStations.count is \(decodedResponse.fuelStations.count)")
//I would iterate through topLevel here and add to the fuelStations
//array but I never get here
} catch {
print(error)
}
}
After you do this , you'll find that some attributes in your struct are coming null in response so you should change string to string? to finally be
struct TopLevel: Codable {
let fuelStations: [FuelStation]
}
struct FuelStation: Codable {
let accessCode, accessDaysTime, accessDetailCode, cardsAccepted,dateLastConfirmed: String?
let id: Int
}
In addition note use of
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
instead of hard-coding the enum

URLSession for XMLParser in SwiftUI returning empty or nil

I am attempting to parse XML using an URLSession, XCode 12, SwiftUI but it keeps returning [] or nil. If I print immediately after the parse(see code), all the data is there, but for some reason, it seems to be clearing it all out.
If I try it with a .xml file, the code works fine, so it must be something in my URLSession in a XMLParserDelegate class:
class ParseController: NSObject, XMLParserDelegate{
var items: [Item] = []
var itemStore: [Item]?
func loadData() {
let url = URL(string: "website")!
let request=URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if data == nil {
print("dataTaskWithRequest error: \(String(describing: error?.localizedDescription))")
return
}
let parser = XMLParser(data: data!)
parser.delegate=self
parser.parse()
self.itemStore=self.items
print(self.itemStore)
}
task.resume()
/*
if let path = Bundle.main.url(forResource: "Items", withExtension: "xml") {
if let parser = XMLParser(contentsOf: path) {
parser.delegate = self
parser.parse()
self.itemStore=self.items
}
}
*/
}
And then I call that with a button in my View:
struct MyParserView: View {
#State var itemsResult: [Item]?
var body: some View {
if ((itemsResult?.isEmpty) == nil) {
VStack {
Text("Stuff here")
Button(action: {
let parserControl = ParseController()
parserControl.loadData()
itemsResult = parserControl.itemStore
}){
Text("Push")
}
}
}else {
List{
ForEach(itemResult!, id:\.id){item in
Text(item.name)
}
}
}
}
}
This was not a problem with XML, but lifecycle in SwiftUI.
https://developer.apple.com/forums/thread/662429
TL;DR: I published itemStore and now works great!

How to decode and get values from API response that is stored in a dictionary in swift

I have an API response that is stored in a dictionary, I have created structs in a different folders but when I try to decode them using decode method it throws an error, the error mentions that "The data couldn’t be read because it isn’t in the correct format." , here is the code and structs I have built:
import UIKit
class ViewController: UIViewController {
#IBOutlet var cityButtons: [UIButton]!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func handleSelection(_ sender: UIButton) {
cityButtons.forEach{ (button) in
UIView.animate(withDuration: 0.3) {
button.isHidden = !button.isHidden
self.view.layoutIfNeeded()
}
}
}
enum Cities:String {
case amman = "Amman"
case azzerqa = "Az zerqa"
case irbid = "Irbid"
case aqaba = "Aqaba"
}
#IBAction func cityTapped(_ sender: UIButton) {
guard let title = sender.currentTitle, let City = Cities(rawValue: title)
else {
return
}
var city:String
switch City {
case .amman:
city = "Amman"
case .azzerqa:
city = "zerqa"
case .irbid:
city = "Irbid"
case .aqaba:
city = "Aqaba"
}
let url = URL(string: "https://api.weatherapi.com/v1/current.json?key={ket}&q=\(city)")
guard url != nil else {
print("error creating URL Object")
return
}
var request = URLRequest(url: url!, cachePolicy: .useProtocolCachePolicy , timeoutInterval: 10)
let headers = ["Content-Type" : "application/json"]
request.allHTTPHeaderFields = headers
request.httpMethod = "GET"
let session = URLSession.shared
let dataTask = session.dataTask(with: request) {(data, response, error) in
if error == nil && data != nil {
do {
let dictionary = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String:Any]
let decoder = JSONDecoder()
print(dictionary)
do {
let weatherdatadecoded = try decoder.decode(WeatherData.self, from: data!)
print(weatherdatadecoded)
}
catch {
print(error.localizedDescription)
}
}
catch {
print(error.localizedDescription)
}
}
}
dataTask.resume()
}
}
and here are the structs (each one is in a separate file):
import Foundation
struct WeatherData : Codable {
var location: Location?
var current: Current?
}
import Foundation
struct Location : Codable {
var name: String = ""
var region: String = ""
var localtime: String = ""
var country: String = ""
}
import Foundation
struct Current : Codable {
var temp_c = 0.0
var is_day = false
var condition: Condition?
}
import Foundation
struct Condition : Codable {
var text: String = ""
var icon: String = ""
var code: Int = 0
}

Braintree Drop-In using SwiftUI

I am trying to set up payment with Braintree, but Braintree does not yet support SwiftUI so I have to integrate it with UIKit. I created a wrapper using UIViewControllerRepresentable and I am presenting it as a modal using the sheet function; however, it does not work as expected, it seems it is opening two modals.
The screen when I open the modal:
Here's my wrapper:
import SwiftUI
import BraintreeDropIn
struct BTDropInRepresentable: UIViewControllerRepresentable {
var authorization: String
var handler: BTDropInControllerHandler
init(authorization: String, handler: #escaping BTDropInControllerHandler) {
self.authorization = authorization
self.handler = handler
}
func makeUIViewController(context: Context) -> BTDropInController {
let bTDropInController = BTDropInController(authorization: authorization, request: BTDropInRequest(), handler: handler)!
return bTDropInController
}
func updateUIViewController(_ uiViewController: BTDropInController, context: UIViewControllerRepresentableContext<BTDropInRepresentable>) {
}
}
Here is where I am trying to open the modal:
Button(action: {
self.checkout = true
}) {
HStack {
Spacer()
Text("Checkout")
.fontWeight(.bold)
.font(.body)
Spacer()
}
.padding(.vertical, 12)
.foregroundColor(.white)
.background(Color.blue)
}.sheet(isPresented: self.$checkout) {
BTDropInRepresentable(authorization: self.token!, handler: { (controller, result, error) in
if (error != nil) {
print("ERROR")
} else if (result?.isCancelled == true) {
print("CANCELLED")
} else if result != nil {
print("SUCCESS")
// Use the BTDropInResult properties to update your UI
// result.paymentOptionType
// result.paymentMethod
// result.paymentIcon
// result.paymentDescription
}
controller.dismiss(animated: true, completion: nil)
})
}
Does anyone have experience with Braintree in SwiftUI or with a similar situation? Am I doing something wrong or forgetting something?
I know writing my own views for Braintree checkout is an option, but I'd like to avoid that.
Thanks!
Have a look at the error you're receiving and I bet it relates to a custom URL scheme that you'll need to add:
Register a URL type
https://developers.braintreepayments.com/guides/paypal/client-side/ios/v4
Also you need to setup your Drop-in payment methods as well, which are all detailed in that guide I linked.
Refer the code for Braintree Payment
// swiftUI
import SwiftUI
import BraintreeCard
import BraintreeDropIn
import BraintreeApplePay
// View
struct ContentView: View {
#StateObject var vm = ViewModel()
#State var methodnoun = ""
#State var viewController = UIViewControllerRep()
var body: some View {
VStack {
Spacer()
// Create ClientToken you have to create your server
Button("create Client") {
//make API call nage hit the server and ClientToken
vm.fetchClientToken()
}
.frame(width: 200, height: 40)
.background(.blue)
.foregroundColor(.white)
Spacer()
Button("Open the Braintree PaymentSheet") {
viewController.showDropIn(clientTokenOrTokenizationKey: vm.clientTolken1)
} .frame(width: 300, height: 40)
.background(.blue)
.foregroundColor(.white)
Spacer()
Spacer()
//loadViewController webView
viewController
Spacer()
}
}
}
// View Model
class ViewModel: ObservableObject {
#Published var clientTolken1: String = ""
#Published var title: String = ""
#Published var messageBody: String = ""
#Published var showSuccessAlert: Bool = false
func fetchClientToken() {
// TODO: Switch this URL to your own authenticated API
let clientTokenURL = NSURL(string: "http://localhost:3000/client_token")!
let clientTokenRequest = NSMutableURLRequest(url: clientTokenURL as URL)
clientTokenRequest.setValue("text/plain", forHTTPHeaderField: "Accept")
URLSession.shared.dataTask(with: clientTokenRequest as URLRequest) { (data, response, error) -> Void in
// TODO: Handle errors
let clientToken = String(data: data!, encoding: String.Encoding.utf8)
self.clientTolken1 = clientToken ?? ""
// self.postNonceToServer(paymentMethodNonce: self.clientTolken1)
// print(clientToken)
// As an example, you may wish to present Drop-in at this point.
// Continue to the next section to learn more...
}.resume()
}
[View Page ][1]
func postNonceToServer(paymentMethodNonce: String) {
// Update URL with your server
do {
let paymentURL = URL(string: "http://localhost:3000/checkout")!
var request = URLRequest(url: paymentURL)
request.httpBody = "payment_method_nonce=\(paymentMethodNonce)".data(using: String.Encoding.utf8)
request.httpMethod = "POST"
URLSession.shared.dataTask(with: request) { (data, response, error) -> Void in
// TODO: Handle success or failure
guard let response = response as? HTTPURLResponse else {
return
}
print("Error Code : \(response.statusCode)")
// let clientToken = String(data: data!, encoding: String.Encoding.utf8)
let json = try? JSONSerialization.jsonObject(with: data ?? Data(), options: [])
print(json)
print("---------------")
let dict = json as? [String: Any]
print(dict?["transaction"])
if (error != nil) {
print("Error : \(error)")
}
print(error?.localizedDescription)
}.resume()
} catch {
print(error.localizedDescription)
}
}
}
// create UIViewControllerRepresentable for integrating UIViewController and Swift
struct UIViewControllerRep: UIViewControllerRepresentable {
let viewController = VmController()
func makeUIViewController(context: Context) -> some VmController {
return viewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
func showDropIn(clientTokenOrTokenizationKey: String) {
viewController.showDropIn(clientTokenOrTokenizationKey: clientTokenOrTokenizationKey)
}
}
// Create ViewController
class VmController: UIViewController {
func showDropIn(clientTokenOrTokenizationKey: String) {
let request = BTDropInRequest()
print(clientTokenOrTokenizationKey)
let dropIn = BTDropInController(authorization: clientTokenOrTokenizationKey, request: request)
{ (controller, result, error) in
if (error != nil) {
print("ERROR")
} else if (result?.isCanceled == true) {
print("CANCELED")
} else if let result = result {
print("Result paymentMethodType : \(result.paymentMethodType)")
print("Result paymentMethod nonce: \(String(describing: result.paymentMethod?.nonce))")
print("Result paymentIcon : \(result.paymentIcon)")
print("Result Description : \(result.paymentDescription)")
}
controller.dismiss(animated: true, completion: nil)
} ?? BTDropInController()
self.present(dropIn, animated: true, completion: nil)
}
}
Braintree WebView
get Card details
refer the official link and setup your client-side and server-side setup.
https://developer.paypal.com/braintree/docs/start/hello-client/

How can I load an UIImage into a SwiftUI Image asynchronously?

In SwiftUI there are some .init methods to create an Image but none of them admits a block or any other way to load an UIImage from network/cache...
I am using Kingfisher to load images from network and cache inside a list row, but the way to draw the image in the view is to re-render it again, which I would prefer to not do. Also, I am creating a fake image(only coloured) as placeholder while the image gets fetched.
Another way would be to wrap all inside a custom view and only re-render the wrapper. But I haven't tried yet.
This sample is working right now.
Any idea to improve the current one will be great
Some view using the loader
struct SampleView : View {
#ObjectBinding let imageLoader: ImageLoader
init(imageLoader: ImageLoader) {
self.imageLoader = imageLoader
}
var body: some View {
Image(uiImage: imageLoader.image(for: "https://url-for-image"))
.frame(width: 128, height: 128)
.aspectRatio(contentMode: ContentMode.fit)
}
}
import UIKit.UIImage
import SwiftUI
import Combine
import class Kingfisher.ImageDownloader
import struct Kingfisher.DownloadTask
import class Kingfisher.ImageCache
import class Kingfisher.KingfisherManager
class ImageLoader: BindableObject {
var didChange = PassthroughSubject<ImageLoader, Never>()
private let downloader: ImageDownloader
private let cache: ImageCache
private var image: UIImage? {
didSet {
dispatchqueue.async { [weak self] in
guard let self = self else { return }
self.didChange.send(self)
}
}
}
private var task: DownloadTask?
private let dispatchqueue: DispatchQueue
init(downloader: ImageDownloader = KingfisherManager.shared.downloader,
cache: ImageCache = KingfisherManager.shared.cache,
dispatchqueue: DispatchQueue = DispatchQueue.main) {
self.downloader = downloader
self.cache = cache
self.dispatchqueue = dispatchqueue
}
deinit {
task?.cancel()
}
func image(for url: URL?) -> UIImage {
guard let targetUrl = url else {
return UIImage.from(color: .gray)
}
guard let image = image else {
load(url: targetUrl)
return UIImage.from(color: .gray)
}
return image
}
private func load(url: URL) {
let key = url.absoluteString
if cache.isCached(forKey: key) {
cache.retrieveImage(forKey: key) { [weak self] (result) in
guard let self = self else { return }
switch result {
case .success(let value):
self.image = value.image
case .failure(let error):
print(error.localizedDescription)
}
}
} else {
downloader.downloadImage(with: url, options: nil, progressBlock: nil) { [weak self] (result) in
guard let self = self else { return }
switch result {
case .success(let value):
self.cache.storeToDisk(value.originalData, forKey: url.absoluteString)
self.image = value.image
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
}
SwiftUI 3
Starting from iOS 15 we can now use AsyncImage:
AsyncImage(url: URL(string: "https://example.com/icon.png")) { image in
image.resizable()
} placeholder: {
ProgressView()
}
.frame(width: 50, height: 50)
SwiftUI 2
Here is a native SwiftUI solution that supports caching and multiple loading states:
import Combine
import SwiftUI
struct NetworkImage: View {
#StateObject private var viewModel = ViewModel()
let url: URL?
var body: some View {
Group {
if let data = viewModel.imageData, let uiImage = UIImage(data: data) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
} else if viewModel.isLoading {
ProgressView()
} else {
Image(systemName: "photo")
}
}
.onAppear {
viewModel.loadImage(from: url)
}
}
}
extension NetworkImage {
class ViewModel: ObservableObject {
#Published var imageData: Data?
#Published var isLoading = false
private static let cache = NSCache<NSURL, NSData>()
private var cancellables = Set<AnyCancellable>()
func loadImage(from url: URL?) {
isLoading = true
guard let url = url else {
isLoading = false
return
}
if let data = Self.cache.object(forKey: url as NSURL) {
imageData = data as Data
isLoading = false
return
}
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.sink { [weak self] in
if let data = $0 {
Self.cache.setObject(data as NSData, forKey: url as NSURL)
self?.imageData = data
}
self?.isLoading = false
}
.store(in: &cancellables)
}
}
}
(The above code doesn't use any third-party libraries, so it's easy to change the NetworkImage in any way.)
Demo
import Combine
import SwiftUI
struct ContentView: View {
#State private var showImage = false
var body: some View {
if showImage {
NetworkImage(url: URL(string: "https://stackoverflow.design/assets/img/logos/so/logo-stackoverflow.png"))
.frame(maxHeight: 150)
.padding()
} else {
Button("Load") {
showImage = true
}
}
}
}
(I used an exceptionally large Stack Overflow logo to show the loading state.)
Pass your Model to ImageRow struct which contains url.
import SwiftUI
import Combine
struct ContentView : View {
var listData: Post
var body: some View {
List(model.post) { post in
ImageRow(model: post) // Get image
}
}
}
/********************************************************************/
// Download Image
struct ImageRow: View {
let model: Post
var body: some View {
VStack(alignment: .center) {
ImageViewContainer(imageUrl: model.avatar_url)
}
}
}
struct ImageViewContainer: View {
#ObjectBinding var remoteImageURL: RemoteImageURL
init(imageUrl: String) {
remoteImageURL = RemoteImageURL(imageURL: imageUrl)
}
var body: some View {
Image(uiImage: UIImage(data: remoteImageURL.data) ?? UIImage())
.resizable()
.clipShape(Circle())
.overlay(Circle().stroke(Color.black, lineWidth: 3.0))
.frame(width: 70.0, height: 70.0)
}
}
class RemoteImageURL: BindableObject {
var didChange = PassthroughSubject<Data, Never>()
var data = Data() {
didSet {
didChange.send(data)
}
}
init(imageURL: String) {
guard let url = URL(string: imageURL) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
DispatchQueue.main.async { self.data = data }
}.resume()
}
}
/********************************************************************/
A simpler and cleaner way to load an image in SwiftUI is to use the renowned Kingfisher library.
Add Kingfisher via Swift Package Manager
Select File > Swift Packages > Add Package Dependency. Enter
https://github.com/onevcat/Kingfisher.git
in the "Choose Package
Repository" dialog. In the next page, specify the version resolving
rule as "Up to Next Major" with "5.8.0" as its earliest version.
After
Xcode checking out the source and resolving the version, you can
choose the "KingfisherSwiftUI" library and add it to your app target.
import KingfisherSwiftUI
KFImage(myUrl)
Done! It's that easy
I would just use the onAppear callback
import Foundation
import SwiftUI
import Combine
import UIKit
struct ImagePreviewModel {
var urlString : String
var width : CGFloat = 100.0
var height : CGFloat = 100.0
}
struct ImagePreview: View {
let viewModel: ImagePreviewModel
#State var initialImage = UIImage()
var body: some View {
Image(uiImage: initialImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: self.width, height: self.height)
.onAppear {
guard let url = URL(string: self.viewModel.urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
guard let image = UIImage(data: data) else { return }
RunLoop.main.perform {
self.initialImage = image
}
}.resume()
}
}
var width: CGFloat { return max(viewModel.width, 100.0) }
var height: CGFloat { return max(viewModel.height, 100.0) }
}
Define the imageLoader as #ObjectBinding:
#ObjectBinding private var imageLoader: ImageLoader
It would make more sense to init the view with the url for the image :
struct SampleView : View {
var imageUrl: URL
private var image: UIImage {
imageLoader.image(for: imageUrl)
}
#ObjectBinding private var imageLoader: ImageLoader
init(url: URL) {
self.imageUrl = url
self.imageLoader = ImageLoader()
}
var body: some View {
Image(uiImage: image)
.frame(width: 200, height: 300)
.aspectRatio(contentMode: ContentMode.fit)
}
}
For example :
//Create a SampleView with an initial photo
var s = SampleView(url: URL(string: "https://placebear.com/200/300")!)
//You could then update the photo by changing the imageUrl
s.imageUrl = URL(string: "https://placebear.com/200/280")!
import SwiftUI
struct UrlImageView: View {
#ObservedObject var urlImageModel: UrlImageModel
init(urlString: String?) {
urlImageModel = UrlImageModel(urlString: urlString)
}
var body: some View {
Image(uiImage: urlImageModel.image ?? UrlImageView.defaultImage!)
.resizable()
.scaledToFill()
}
static var defaultImage = UIImage(systemName: "photo")
}
class UrlImageModel: ObservableObject {
#Published var image: UIImage?
var urlString: String?
init(urlString: String?) {
self.urlString = urlString
loadImage()
}
func loadImage() {
loadImageFromUrl()
}
func loadImageFromUrl() {
guard let urlString = urlString else {
return
}
let url = URL(string: urlString)!
let task = URLSession.shared.dataTask(with: url, completionHandler:
getImageFromResponse(data:response:error:))
task.resume()
}
func getImageFromResponse(data: Data?, response: URLResponse?, error: Error?)
{
guard error == nil else {
print("Error: \(error!)")
return
}
guard let data = data else {
print("No data found")
return
}
DispatchQueue.main.async {
guard let loadedImage = UIImage(data: data) else {
return
}
self.image = loadedImage
}
}
}
And using like this:
UrlImageView(urlString: "https://developer.apple.com/assets/elements/icons/swiftui/swiftui-96x96_2x.png").frame(width:100, height:100)
With the release of iOS 15 and macOS 12 in 2021, SwiftUI provides native AsyncImage view that enables loading images asynchronously. Bear in mind that you'll still have to fall back to a custom implementation for earlier OS versions.
AsyncImage(url: URL(string: "https://example.com/tile.png"))
The API itself also provides various ways to customise the image or provide a placeholder, for example:
AsyncImage(url: URL(string: "https://example.com/tile.png")) { image in
image.resizable(resizingMode: .tile)
} placeholder: {
Color.green
}
More in the Apple Developer Documentation.

Resources