I am trying to call model form #main App where the model has the dependency on a repository with init function. The repository has the URLSession and Baseurl properties . I have passed the required property on both approach ..
Here is approach I have tried based on Xcode suggestions ..
#main
struct HomwWorkWithSwiftUIApp: App {
#StateObject var model = FruitsModel(fruitRepository: FruitsRepository.self as! FruitsRepository)
var body: some Scene {
WindowGroup {
ContentView().environmentObject(model)
}
}
}
As a result as was crashed at run time with error Thread 1: signal SIGABRT
The second approach is passing the require parameters like this ..
#main
struct HomwWorkWithSwiftUIApp: App {
#StateObject var model = FruitsModel(fruitRepository: RealFruitsRepository(session: URLSession, baseURL: EndPoint.baseUrl))
var body: some Scene {
WindowGroup {
ContentView().environmentObject(model)
}
}
}
It giving error ..Cannot convert value of type 'URLSession.Type' to expected argument type 'URLSession'
Here is attempt for URLSession instance.
#main
struct HomwWorkWithSwiftUIApp: App {
init() {
}
var url : URLSession
init(url: URLSession) {
self.url = url
}
#StateObject var model = FruitsModel(fruitRepository: RealFruitsRepository(session: url, baseURL: EndPoint.baseUrl))
var body: some Scene {
WindowGroup {
ContentView().environmentObject(model)
}
}
}
Here is the screenshot ..
Here is the repository code ..
import Foundation
protocol FruitsRepository: WebRepository {
func loadFruits() async throws -> [Fruits]
}
struct RealFruitsRepository: FruitsRepository {
let session: URLSession
let baseURL: String
init(session: URLSession, baseURL: String) {
self.session = session
self.baseURL = baseURL
}
func loadFruits() async throws -> [Fruits] {
guard let request = try? API.allFruits.urlRequest(baseURL: baseURL) else {
throw APIError.invalidURL
}
guard let data = try? await call(request: request) else {
throw APIError.unexpectedResponse
}
guard let fruits = getDecodedFruitesResopnse(from: data) else {
throw APIError.unexpectedResponse
}
return fruits
}
private func getDecodedFruitesResopnse(from data: Data)-> [Fruits]? {
guard let fruites = try? JSONDecoder().decode([Fruits].self, from: data) else {
return nil
}
return fruites
}
}
extension RealFruitsRepository {
enum API {
case allFruits
case fruitDetails(Fruits)
}
}
extension RealFruitsRepository.API: APICall {
var path: String {
switch self {
case .allFruits:
return "/all"
case let .fruitDetails(fruit):
let encodedName = fruit.name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
return "/name/\(encodedName ?? fruit.name)"
}
}
var method: String {
switch self {
case .allFruits, .fruitDetails:
return "GET"
}
}
var headers: [String: String]? {
return ["Accept": "application/json"]
}
func body() throws -> Data? {
return nil
}
}
Here is the model class ..
import Foundation
import Combine
protocol FruitsModelInput {
func getFruits() async
}
protocol FruitsModelOutput {
var state: FruitViewStates { get }
var fruitRecordsCount: Int { get }
func getFruit(index: Int)-> Fruits
func getFruitsDetails(for row:Int)-> FruitsDetails
}
struct FruitsDetails {
let genus, name: String
}
final class FruitsModel: ObservableObject {
private var fruitsRepository: FruitsRepository
var fruits: [Fruits] = []
#Published var state: FruitViewStates = .none
private var cancellables:Set<AnyCancellable> = Set()
init(fruitRepository: FruitsRepository) {
self.fruitsRepository = fruitRepository
}
}
extension FruitsModel: FruitsModelOutput {
func getFruitsDetails(for row: Int) -> FruitsDetails {
if row >= 0 {
let fruit = fruits[row]
return FruitsDetails(genus: fruit.genus, name: fruit.name)
}
return FruitsDetails(genus: "", name: "")
}
var fruitRecordsCount: Int {
return fruits.count
}
func getFruit(index: Int) -> Fruits {
if fruits.count > 0 {
return (fruits[index])
} else {
return Fruits(genus: "", name: "", id: 0, family: "", order: "", nutritions: Nutritions(carbohydrates: 0.0, protein: 0.0, fat: 0.0, calories: 0, sugar: 0.0))
}
}
}
extension FruitsModel: FruitsModelInput {
func getFruits() async {
state = .showActivityIndicator
do {
fruits = try await fruitsRepository.loadFruits()
self.state = .showFruitList
} catch let error {
fruits = []
print(error)
state = .showError((error as! APIError).localizedDescription)
}
}
}
Related
I have a model with data string of name and a bool of UE. I'm trying to display item.name whenever UE is true. My issue is when whenever I run the code the data doesn't seem to read the UE. I got an error displaying the item.UE as a text view. The data that I am getting it from is from a database the item.name works without the conditional.
struct AttendingUsersView: View {
#ObservedObject var model = UserViewModel()
var body: some View {
VStack {
List (model.list) { item in
if item.UE == true {
Text(item.name)
} else {
Text("This isnt working")
}
}
DismissButton
}
}
I've tried displaying the item.UE to see what it would display but I get an error saying "No exact matches in call to initializer".
UserViewModel file
class UserViewModel: ObservableObject {
#Published var list = [Username]()
func addData(name: String, UE: Bool) {
//get a reference to the database
let db = Firestore.firestore()
// Add a new document in collection "username"
db.collection("usernames").document(UserDefaults.standard.object(forKey: "value") as! String).setData([
// MARK: Change the parameters to the users inputed choices
"name": name,
"UE": UE
]) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
}
func getData() {
//get a reference to the database
let db = Firestore.firestore()
//Read the documents at a specific path
db.collection("usernames").getDocuments { snapshot, error in
//checking for errors
if error == nil {
//no errors
if let snapshot = snapshot {
// update
DispatchQueue.main.async {
// Get all the documents and create usernames
self.list = snapshot.documents.map { d in
//Create a Username
return Username(id: d.documentID, name: d["name"] as? String ?? "", UE: (d["UE"] != nil)) //cast as a string and if not found return as a empty string
}
}
}
} else {
//Handle the error
}
}
}
}
Username model
struct Username: Identifiable {
var id: String
var name: String
var ue: Bool
}
Try this, with fixes for your ue in your getData, and
in the view display.
struct ContentView: View {
var body: some View {
AttendingUsersView()
}
}
struct AttendingUsersView: View {
#StateObject var model = UserViewModel() // <-- here
var body: some View {
VStack {
List (model.list) { item in
if item.ue { // <-- here
Text(item.name)
} else {
Text("This is working ue is false")
}
}
// DismissButton
}
}
}
class UserViewModel: ObservableObject {
// for testing
#Published var list:[Username] = [Username(id: "1", name: "item-1", ue: false),
Username(id: "2", name: "item-2", ue: true),
Username(id: "3", name: "item-3", ue: false)]
func addData(name: String, UE: Bool) {
//get a reference to the database
let db = Firestore.firestore()
// Add a new document in collection "username"
db.collection("usernames").document(UserDefaults.standard.object(forKey: "value") as! String).setData([
// MARK: Change the parameters to the users inputed choices
"name": name,
"UE": UE
]) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
}
func getData() {
//get a reference to the database
let db = Firestore.firestore()
//Read the documents at a specific path
db.collection("usernames").getDocuments { snapshot, error in
//checking for errors
if error == nil {
//no errors
if let snapshot = snapshot {
// update
DispatchQueue.main.async {
// Get all the documents and create usernames
self.list = snapshot.documents.map { d in
//Create a Username
// -- here, ue:
return Username(id: d.documentID, name: d["name"] as? String ?? "", ue: (d["UE"] != nil)) // <-- here ue:
}
}
}
} else {
//Handle the error
}
}
}
}
struct Username: Identifiable {
var id: String
var name: String
var ue: Bool
}
Here is my async function class:
class MoviesViewModel: ObservableObject {
#Published var topRated: [Movie] = []
#Published var popular: [Movie] = []
#Published var upcoming: [Movie] = []
func getUpcomingMovies() {
if let movies = getMovies(path: "upcoming") {
DispatchQueue.main.async {
self.upcoming = movies
}
}
}
func getPopularMovies() {
if let movies = getMovies(path: "popular") {
DispatchQueue.main.async {
self.popular = movies
}
}
}
func getTopRatedMovies() {
DispatchQueue.main.async {
if let movies = self.getMovies(path: "top_rated") {
self.topRated = movies
}
}
}
func getMovies(path: String) -> [Movie]? {
var movies: [Movie]?
let urlString = "https://api.themoviedb.org/3/movie/\(path)?api_key=\(apiKey)&language=en-US&page=1"
guard let url = URL(string: urlString) else { return [] }
let session = URLSession.shared
let dataTask = session.dataTask(with: url, completionHandler: { data, _, error in
if error != nil {
print(error)
}
do {
if let safeData = data {
let decodedData = try JSONDecoder().decode(NowPlaying.self, from: safeData)
DispatchQueue.main.async {
movies = decodedData.results
}
}
}
catch {
print(error)
}
})
dataTask.resume()
return movies
}
}
When I printed the movies in getMovies function, I can get movies from api without problem. However, UI does not update itself. I used DispatchQueue.main.async function but it did not solve my problem. What can I do in this situation?
dataTask works asynchronously. Your code returns nil even before the asynchronous task is going to start. You have to use a completion handler as described in Returning data from async call in Swift function.
I highly recommend to use async/await in this case. You get rid of a lot of boilerplate code and you don't need to care about dispatching threads.
#MainActor
class MoviesViewModel: ObservableObject {
#Published var topRated: [Movie] = []
#Published var popular: [Movie] = []
#Published var upcoming: [Movie] = []
func getUpcomingMovies() async throws {
self.upcoming = try await getMovies(path: "upcoming")
}
func getPopularMovies() async throws {
self.popular = try await getMovies(path: "popular")
}
func getTopRatedMovies() async throws {
self.topRated = try await getMovies(path: "top_rated")
}
func getMovies(path: String) async throws -> [Movie] {
let urlString = "https://api.themoviedb.org/3/movie/\(path)?api_key=\(apiKey)&language=en-US&page=1"
guard let url = URL(string: urlString) else { throw URLError(.badURL) }
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(NowPlaying.self, from: data).results
}
}
Code from tutorial, want to access contacts from my app, and using this as base
Need to solve this error to move forward and do not know what additional items needs initialization. Any help very appreciated.
import Contacts
final class ContactsViewModel: ObservableObject {
#Published
var contact: [Contact?]
#Published
var contacts: [Contact?] = []
#Published
var permissionsError: PermissionsError? = .none
init( contact: Contact, contacts: [Contact] , permissionsError: PermissionsError) {
permissions()
}. **<-- Error occurs here -**
func openSettings() {
permissionsError = .none
guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else {return}
if UIApplication.shared.canOpenURL(settingsURL) { UIApplication.shared.open(settingsURL)}
}
func getContacts() {
Contact.fetchAll { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let fetchedContacts):
DispatchQueue.main.async {
self.contacts = fetchedContacts.sorted(by: { $0.lastName < $1.lastName })
}
case .failure(let error):
self.permissionsError = .fetchError(error)
}
}
}
func permissions() {
switch CNContactStore.authorizationStatus(for: .contacts) {
case.authorized:
getContacts()
case .notDetermined, .restricted, .denied :
CNContactStore().requestAccess(for: .contacts) { [weak self] granted, error in
switch granted {
case true: self?.getContacts()
case false:
DispatchQueue.main.async {
self?.permissionsError = .userError
}
}
}
There are a number of examples showing how to do reverse geolocation, but nothing recent on implementation in SwiftUI. My current code uses the iPhone GPS to generate coordinates that are used with maps to show the location. I would also like to display the street address since a map without text indicating the location isn't very helpful.
My Questions:
Do I have all the relevant code to implement reverse geolocation?
I have seen examples using storyboards and print statements to display the location, but how do I return the location to a Swiftui view with an #escaping closure?
import Foundation
import CoreLocation
class LocationManager: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
#Published var currentAddress: String = ""
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.distanceFilter = 10 // distance before update (meters)
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.startUpdatingLocation()
}
func startLocationServices() {
if locationManager.authorizationStatus == .authorizedAlways || locationManager.authorizationStatus == .authorizedWhenInUse {
locationManager.startUpdatingLocation()
} else {
locationManager.requestWhenInUseAuthorization()
}
}
func getLocationCoordinates() -> (Double, Double) {
let coordinate = self.locationManager.location != nil ? self.locationManager.location!.coordinate : CLLocationCoordinate2D()
print("location = \(coordinate.latitude), \(coordinate.longitude)")
return (Double(coordinate.latitude), Double(coordinate.longitude))
}
// Using closure
func getAddress(handler: #escaping (String) -> Void)
{
self.currentAddress = ""
let coordinate = self.locationManager.location != nil ? self.locationManager.location!.coordinate : CLLocationCoordinate2D()
let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, error) -> Void in
// Place details
var placeMark: CLPlacemark?
placeMark = placemarks?[0]
guard let placemark = placemarks?.first else { return }
if let streetNumber = placemark.subThoroughfare,
let street = placemark.subThoroughfare,
let city = placemark.locality,
let state = placemark.administrativeArea {
DispatchQueue.main.async {
self.currentAddress = "\(streetNumber) \(street) \(city) \(state)"
}
} else if let city = placemark.locality, let state = placemark.administrativeArea {
DispatchQueue.main.async {
self.currentAddress = "\(city) \(state)"
}
} else {
DispatchQueue.main.async {
self.currentAddress = "Address Unknown"
}
}
}
)
print( self.currentAddress)
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
if locationManager.authorizationStatus == .authorizedAlways || locationManager.authorizationStatus == .authorizedWhenInUse {
locationManager.startUpdatingLocation()
}
}
// Get Placemark
func getPlace(for location: CLLocation,
completion: #escaping (CLPlacemark?) -> Void) {
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { placemarks, error in
guard error == nil else {
print("*** Error in \(#function): \(error!.localizedDescription)")
completion(nil)
return
}
guard let placemark = placemarks?[0] else {
print("*** Error in \(#function): placemark is nil")
completion(nil)
return
}
completion(placemark)
}
}
}
If I add the follow code say in ContentView:
#State private var entryLat: Double = 0.0
#State private var entryLong: Double = 0.0
let result = lm.getLocationCoordinates()
entryLat = result.0
entryLong = result.1
How would I call getPlace?
To use the following code you need to setup the appropriate entitlements and authorizations.
Here is a working example of using geolocation in swiftui, from code I got from
a number of sources on the net years ago.
This should give you a base to do reverse geolocation in swiftui:
import Foundation
import CoreLocation
import SwiftUI
import Combine
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
let locationProvider = LocationProvider()
#State var currentAddress = ""
var body: some View {
Text(currentAddress)
.onAppear {
getAddress()
}
}
func getAddress() {
// for testing Tokyo
let location = CLLocation(latitude: 35.684602, longitude: 139.751992)
locationProvider.getPlace(for: location) { plsmark in
guard let placemark = plsmark else { return }
if let streetNumber = placemark.subThoroughfare,
let street = placemark.subThoroughfare,
let city = placemark.locality,
let state = placemark.administrativeArea {
self.currentAddress = "\(streetNumber) \(street) \(city) \(state)"
} else if let city = placemark.locality, let state = placemark.administrativeArea {
self.currentAddress = "\(city) \(state)"
} else {
self.currentAddress = "Address Unknown"
}
}
}
}
/**
A Combine-based CoreLocation provider.
On every update of the device location from a wrapped `CLLocationManager`,
it provides the latest location as a published `CLLocation` object and
via a `PassthroughSubject<CLLocation, Never>` called `locationWillChange`.
*/
public class LocationProvider: NSObject, ObservableObject {
private let lm = CLLocationManager()
/// Is emitted when the `location` property changes.
public let locationWillChange = PassthroughSubject<CLLocation, Never>()
/**
The latest location provided by the `CLLocationManager`.
Updates of its value trigger both the `objectWillChange` and the `locationWillChange` PassthroughSubjects.
*/
#Published public private(set) var location: CLLocation? {
willSet {
locationWillChange.send(newValue ?? CLLocation())
}
}
/// The authorization status for CoreLocation.
#Published public var authorizationStatus: CLAuthorizationStatus?
/// A function that is executed when the `CLAuthorizationStatus` changes to `Denied`.
public var onAuthorizationStatusDenied : ()->Void = {presentLocationSettingsAlert()}
/// The LocationProvider intializer.
///
/// Creates a CLLocationManager delegate and sets the CLLocationManager properties.
public override init() {
super.init()
self.lm.delegate = self
self.lm.desiredAccuracy = kCLLocationAccuracyBest
self.lm.activityType = .fitness
self.lm.distanceFilter = 10
self.lm.allowsBackgroundLocationUpdates = true
self.lm.pausesLocationUpdatesAutomatically = false
self.lm.showsBackgroundLocationIndicator = true
}
/**
Request location access from user.
In case, the access has already been denied, execute the `onAuthorizationDenied` closure.
The default behavior is to present an alert that suggests going to the settings page.
*/
public func requestAuthorization() -> Void {
if self.authorizationStatus == CLAuthorizationStatus.denied {
onAuthorizationStatusDenied()
}
else {
self.lm.requestWhenInUseAuthorization()
}
}
/// Start the Location Provider.
public func start() throws -> Void {
self.requestAuthorization()
if let status = self.authorizationStatus {
guard status == .authorizedWhenInUse || status == .authorizedAlways else {
throw LocationProviderError.noAuthorization
}
}
else {
/// no authorization set by delegate yet
#if DEBUG
print(#function, "No location authorization status set by delegate yet. Try to start updates anyhow.")
#endif
/// In principle, this should throw an error.
/// However, this would prevent start() from running directly after the LocationProvider is initialized.
/// This is because the delegate method `didChangeAuthorization`,
/// setting `authorizationStatus` runs only after a brief delay after initialization.
//throw LocationProviderError.noAuthorization
}
self.lm.startUpdatingLocation()
}
/// Stop the Location Provider.
public func stop() -> Void {
self.lm.stopUpdatingLocation()
}
// todo deal with errors
public func getPlace(for location: CLLocation, completion: #escaping (CLPlacemark?) -> Void) {
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { placemarks, error in
guard error == nil else {
print("=====> Error \(error!.localizedDescription)")
completion(nil)
return
}
guard let placemark = placemarks?.first else {
print("=====> Error placemark is nil")
completion(nil)
return
}
completion(placemark)
}
}
}
/// Present an alert that suggests to go to the app settings screen.
public func presentLocationSettingsAlert(alertText : String? = nil) -> Void {
let alertController = UIAlertController (title: "Enable Location Access", message: alertText ?? "The location access for this app is set to 'never'. Enable location access in the application settings. Go to Settings now?", preferredStyle: .alert)
let settingsAction = UIAlertAction(title: "Settings", style: .default) { (_) -> Void in
guard let settingsUrl = URL(string:UIApplication.openSettingsURLString) else {
return
}
UIApplication.shared.open(settingsUrl)
}
alertController.addAction(settingsAction)
let cancelAction = UIAlertAction(title: "Cancel", style: .default, handler: nil)
alertController.addAction(cancelAction)
UIApplication.shared.windows[0].rootViewController?.present(alertController, animated: true, completion: nil)
}
/// Error which is thrown for lacking localization authorization.
public enum LocationProviderError: Error {
case noAuthorization
}
extension LocationProvider: CLLocationManagerDelegate {
public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
self.authorizationStatus = status
#if DEBUG
print(#function, status.name)
#endif
//print()
}
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
self.location = location
}
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
if let clErr = error as? CLError {
switch clErr {
case CLError.denied : do {
print(#function, "Location access denied by user.")
self.stop()
self.requestAuthorization()
}
case CLError.locationUnknown : print(#function, "Location manager is unable to retrieve a location.")
default: print(#function, "Location manager failed with unknown CoreLocation error.")
}
}
else {
print(#function, "Location manager failed with unknown error", error.localizedDescription)
}
}
}
extension CLAuthorizationStatus {
/// String representation of the CLAuthorizationStatus
var name: String {
switch self {
case .notDetermined: return "notDetermined"
case .authorizedWhenInUse: return "authorizedWhenInUse"
case .authorizedAlways: return "authorizedAlways"
case .restricted: return "restricted"
case .denied: return "denied"
default: return "unknown"
}
}
}
I have a view that listens to a Model via and ObservableObject:
class Feed : ObservableObject {
// Posts to be displayed
#Published var posts = [Posts]()
...
...
}
And the Posts model looks like:
struct Posts: Hashable, Identifiable {
let bar: Bars
let time: String
var description: String
let id: String
let createdAt : String
let tags : [Friends]
let groups : [String]
var intializer : Friends // Creator of the post
}
Which contains multiple other Struct models like Friends and Bars. However, when I do change a value within one of these other models, it doesn't trigger the #Published to fire, so the view isn't redrawn. For example, the Friends model looks like:
struct Friends : Hashable {
static func == (lhs: Friends, rhs: Friends) -> Bool {
return lhs.id == rhs.id
}
let name: String
let username: String
let id : String
var thumbnail : UIImage?
var profileImgURL : String?
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
but when I change the thumbnail, the views are not redrawn. But when I change something directly apart of the Posts model, like the description attribute, the view is redrawn. How am I able to have the view redraw when the underlying model values are changed?
I change the thumbnail as shown:
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
self.posts[indexOfPost].intializer.thumbnail = UIImage(data: data)
}
}
}
}
But if I were to change the description doing the same thing:
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
self.posts[indexOfPost].description = "Loaded!!!!"
}
}
}
}
And when I do this, the view does update and change. I can see that the thumbnails are being loaded correctly, too, because I can print out the data sent, and sometimes the thumbnails are redrawn for the view correctly.
EDIT
As suggested I tried adding a mutating func to the struct:
struct Posts: Hashable, Identifiable {
let bar: Bars
let time: String
var description: String
let id: String
let createdAt : String
let tags : [Friends]
let groups : [String]
var intializer : Friends // Creator of the post
mutating func addInitThumbnail(img : UIImage) {
self.intializer.thumbnail = img
}
}
and then using it:
func grabInitThumbnail(post : Posts) {
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
if let thumbnailImg = UIImage(data: data) {
self.posts[indexOfPost].addInitThumbnail(img: thumbnailImg)
}
}
}
}
}
}
but it did not work either.
However, when I do:
func grabInitThumbnail(post : Posts) {
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
self.posts[indexOfPost].intializer.thumbnail = UIImage(data: data)
self.posts[indexOfPost].description = "Loaded!!!!"
}
}
}
}
}
the images are loaded and set correctly...? So I think it might have something to do with UIImages directly?
I tried using mutating function and also updating value directly, both cases it worked.
UPDATED CODE (Added UIImage in new struct)
import SwiftUI
import Foundation
//Employee
struct Employee : Identifiable{
var id: String = ""
var name: String = ""
var address: Address
var userImage: UserIcon
init(name: String, id: String, address: Address, userImage: UserIcon) {
self.id = id
self.name = name
self.address = address
self.userImage = userImage
}
mutating func updateAddress(with value: Address){
address = value
}
}
//User profile image
struct UserIcon {
var profile: UIImage?
init(profile: UIImage) {
self.profile = profile
}
mutating func updateProfile(image: UIImage) {
self.profile = image
}
}
//Address
struct Address {
var houseName: String = ""
var houseNumber: String = ""
var place: String = ""
init(houseName: String, houseNumber: String, place: String) {
self.houseName = houseName
self.houseNumber = houseNumber
self.place = place
}
func getCompleteAddress() -> String{
let addressArray = [self.houseName, self.houseNumber, self.place]
return addressArray.joined(separator: ",")
}
}
//EmployeeViewModel
class EmployeeViewModel: ObservableObject {
#Published var users : [Employee] = []
func initialize() {
self.users = [Employee(name: "ABC", id: "100", address: Address(houseName: "Beautiful Villa1", houseNumber: "17ABC", place: "USA"), userImage: UserIcon(profile: UIImage(named: "discover")!)),
Employee(name: "XYZ", id: "101", address: Address(houseName: "Beautiful Villa2", houseNumber: "18ABC", place: "UAE"), userImage: UserIcon(profile: UIImage(named: "discover")!)),
Employee(name: "QWE", id: "102", address: Address(houseName: "Beautiful Villa3", houseNumber: "19ABC", place: "UK"), userImage: UserIcon(profile: UIImage(named: "discover")!))]
}
func update() { //both below cases worked
self.users[0].address.houseName = "My Villa"
//self.users[0].updateAddress(with: Address(houseName: "My Villa", houseNumber: "123", place: "London"))
self.updateImage()
}
func updateImage() {
self.users[0].userImage.updateProfile(image: UIImage(named: "home")!)
}
}
//EmployeeView
struct EmployeeView: View {
#ObservedObject var vm = EmployeeViewModel()
var body: some View {
NavigationView {
List {
ForEach(self.vm.users) { user in
VStack {
Image(uiImage: user.userImage.profile!)
Text("\(user.name) - \(user.address.getCompleteAddress())")
}
}.listRowBackground(Color.white)
}.onAppear(perform: fetch)
.navigationBarItems(trailing:
Button("Update") {
self.vm.update()
}.foregroundColor(Color.blue)
)
.navigationBarTitle("Users", displayMode: .inline)
}.accentColor(Color.init("blackTextColor"))
}
func fetch() {
self.vm.initialize()
}
}
it's been a long time but still :
1 - mutating func is not necessary.
2 - The re-rendering won't happen if you only change the nested object and not the "observed" object it self.
3 - You can play with the getters and setters as well, to pull the wanted value to change and update it back.
Considering we have a complex object such as :
struct Content{
var listOfStuff : [Any] = ["List", 2, "Of", "Stuff"]
var isTheSkyGrey : Bool = false
var doYouLikeMyMom : Bool = false
var status : UIImage? = UIImage(systemName: "paperplane")
}
Now let's wrap/nest this object into a ContentModel for the View. If, while using the #State var contentModel : ContentModel in the View, we change change one of the properties directly by accessing the nested object(like so : model.content.status = "Tchak"), it will not trigger a re-rendering because the ContentModel itself didn't change.
Understanding this, we need to trigger a tiny useless change in the ContentModel :
struct ContentModel {
private var change : Bool = false
private var content : Content = Content() {
didSet{
// this will trigger the view to re-render
change.toggle()
}
}
//the value you want to change
var status : UIImage?{
get{
contentModel.status
}
set{
contentModel.status = newValue
}
}
}
Now what's left to do is to observe the change of the content in the view.
struct ContentPouf: View {
#State var contentModel = ContentModel()
var body: some View {
Image(uiImage: contentModel.status)
.onTapGesture {
contentModel.status = UIImage(systemName: "pencil")
}
}
}
and using an ObservableObject it would be :
class ContentObservable : ObservableObject {
#Published var content : ContentModel = ContentModel()
func handleTap(){
content.status = UIImage(systemName: "pencil")
}
}
and
#StateObject var viewModel : ContentObservable = ContentObservable()
var body: some View {
Image(uiImage :viewModel.content.status)
.onTapGesture {
viewModel.handleTap()
}
}