I am new to SwiftUI and I am trying to encode and decode a MKPlacemark struct to json.
I have the struct defined as below. I am able to display the details in the app but I am not able to decode it.
import Foundation
import MapKit
import UIKit
struct Landmark {
let placemark: MKPlacemark
var id: UUID {
return UUID()
}
var name: String {
self.placemark.name ?? ""
}
var title: String {
self.placemark.title ?? ""
}
var coordinate: CLLocationCoordinate2D {
self.placemark.coordinate
}
}
I can search for placemarks like this:
import Foundation
import Combine
import MapKit
class SearchPlaces: NSObject, ObservableObject {
#Published var searchQuery = ""
#Published var landmarks: [Landmark] = [Landmark]()
#Published var items: [MapItem] = [MapItem]()
public func getNearByLandmarks() {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = searchQuery
let search = MKLocalSearch(request: request)
search.start { (response, error) in
if let response = response {
let mapItems = response.mapItems
self.landmarks = mapItems.map {
Landmark(placemark: $0.placemark)
}
Task {
await self.getData()
}
print("Lamdmarks \(self.landmarks)")
}
}
}
private func getData() async {
guard let landmark = try? JSONEncoder().encode(self.landmarks) else { return }
do {
let decodedLandmark = try JSONDecoder().decode(Landmark.self, from: landmark)
print("decodedLandmark \(decodedLandmark.id)")
} catch {
print("Error \(error.localizedDescription)")
}
}
}
But I get this error: Error
The data couldn’t be read because it isn’t in the correct format.
The placemark looks like this in xcode
Lamdmarks \[Landmark(placemark: La Hacienda Market, 249 Hillside Blvd, South San Francisco, CA 94080, United States # \<+37.66312925,-122.40844847\> +/- 0.00m, region CLCircularRegion (identifier:'\<+37.66307481,-122.40861130\> radius 141.17', center:\<+37.66307481,-122.40861130\>, radius:141.17m))
How do I decode a MKPlacemark to json when I don't know all of its keys.
I tried this
extension NSSecureCoding { func archived() throws -> Data { try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false) } }
extension Data { func unarchived<T: NSSecureCoding>() throws -> T? { try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(self) as? T } }
extension Landmark: Codable {
func encode(to encoder: Encoder) throws {
var unkeyedContainer = encoder.unkeyedContainer()
try unkeyedContainer.encode(placemark.archived())
try unkeyedContainer.encode(id)
try unkeyedContainer.encode(name)
try unkeyedContainer.encode(title)
try unkeyedContainer.encode(coordinate)
}
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
placemark = try container.decode(Data.self).unarchived()!
coordinate = try container.decode(CLLocationCoordinate2D.self, "coordinate")
id = try container.decode(UUID.self)
name = placemark.name ?? "no name"
title = placemark.title ?? "no title"
}
}
First of all never print(error.localizedDescription) in a Codable context. The generic error message is meaningless.
Always
print(error)
to get the real meaningful DecodingError.
Second of all don't try to adopt Codable by serializing each single property in classes which conform to NSSecureCoding. Take advantage of the built-in serialization and also of the PropertyWrapper pattern.
This PropertyWrapper converts/serializes MKPlacemark to Data and vice versa
#propertyWrapper
struct CodablePlacemark {
var wrappedValue: MKPlacemark
}
extension CodablePlacemark: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let data = try container.decode(Data.self)
guard let placemark = try NSKeyedUnarchiver.unarchivedObject(ofClass: MKPlacemark.self, from: data) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid placemark"
)
}
wrappedValue = placemark
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let data = try NSKeyedArchiver.archivedData(withRootObject: wrappedValue, requiringSecureCoding: true)
try container.encode(data)
}
}
In the Landmark struct adopt Codable and declare the placemark
struct Landmark: Codable {
#CodablePlacemark var placemark: MKPlacemark
}
But the property wrapper makes only sense if you encode the placemark.
Related
I have an async/await function to make sure that the data gets passed along first;
func downloadFirebaseData() async -> String {
let group = DispatchGroup()
group.enter() // stop the thread/enter the function
let db = Firestore.firestore()
withUnsafeThrowingContinuation { continuation in
db.collection("annotations")
.getDocuments { (querySnapshot, error) in
defer {
group.leave() // << end on any return
}
if let Lng = i.document.get("lng") as? String {
DispatchQueue.main.async {
annotationLng.append(Lng) //edit the array
print("downloadLngServerData ()\(annotationLng)")
}
}
if let Lat = i.document.get("lat") as? String {
DispatchQueue.main.async {
annotationLat.append(Lat) //edit the array
print("downloadLatServerData ()\(annotationLat)")
}
}
}
}
group.wait() // clear up the thread now, exit the function
}
And its called under my view with;
.task {
try await downloadFirebaseData() //error 1
}
#State annotationLat: [String] = []
#State annotationLng: [String] = []
Inside of firebase database:
annotationLat = ["42.828392","29.18273","97.27352"]
annotationLng = ["42.828392","29.18273","97.27352"]
I have 2 errors;
Invalid conversion from throwing function of type '#Sendable () async throws -> Void' to non-throwing function type '#Sendable () async -> Void'
This was under the .task
My second error:
Generic parameter 'T' could not be inferred
This was under withUnsafeThrowingContinuation
The first error I somewhat get, but even after I modified from my original code, the error still persisted.
For the second error, I know that I might have to define that this is a string somewhere, because I don't think that the app knows that I'm trying to work with a string.
This assumes that the Firestore path for the documents is
annotations/{id}
and that each document has variables lat and lng of type String
import Foundation
import FirebaseFirestoreSwift
import FirebaseFirestore
import CoreLocation
//struct to keep the latitude and longitude together, they should not be in separate arrays
struct Annotation: Codable, Identifiable{
#DocumentID var id: String?
var lat: String?
var lng: String?
}
extension Annotation{
//Safely unwrap the Strings into doubles and then create the coordinate
var coordinate: CLLocationCoordinate2D? {
guard let latStr = lat, let lngStr = lng, let latitude = Double(latStr), let longitude = Double(lngStr) else{
print("Unable to get valid latitude and longitude")
return nil
}
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
return coordinate
}
}
struct CustomFirestoreService{
let store: Firestore = .firestore()
init(){}
func getAnnotations() async throws -> [Annotation]{
let ANNOTATIONS_PATH = "annotations"
return try await retrieve(path: ANNOTATIONS_PATH)
}
///retrieves all the documents in the collection at the path
private func retrieve<FC : Codable>(path: String) async throws -> [FC]{
//Firebase provided async await.
let querySnapshot = try await store.collection(path).getDocuments()
return querySnapshot.documents.compactMap { document in
do{
return try document.data(as: FC.self)
}catch{
print(error)
return nil
}
}
}
}
Then in your View
import SwiftUI
struct AnnotationsView: View {
let service: CustomFirestoreService = CustomFirestoreService()
#State private var annotations: [Annotation] = []
var body: some View {
if annotations.isEmpty{
Text("Hello, World!")
.task {
do{
annotations = try await service.getAnnotations()
//Do any other work here, this line won't run unless the annotations are populated.
}catch{
print(error)
}
}
}else{
List(annotations){ annotation in
if let coord = annotation.coordinate{
VStack{
Text("Latitude = \(coord.latitude)")
Text("Longitude = \(coord.longitude)")
}
}else{
Text("Invalid Coordinate Value. Check firestore values for document \(annotation.id ?? "no id")")
}
}
}
}
}
struct AnnotationsView_Previews: PreviewProvider {
static var previews: some View {
AnnotationsView()
}
}
This makes some assumptions but if you paste it into your project you should get some working code.
You don't need this for your code but this is what a conversion from the "old" closures to the new async await would look like.
public func retrieve<FC : Codable>(path: String) async throws -> [FC]{
typealias MyContinuation = CheckedContinuation<[FC], Error>
return try await withCheckedThrowingContinuation { (continuation: MyContinuation) in
store.collection(path)
.getDocuments() { (querySnapshot, err) in
if let err = err {
//This throws an error
continuation.resume(throwing: err)
} else {
let array = querySnapshot?.documents.compactMap { document in
try? document.data(as: FC.self)
} ?? []
//This returns an array
continuation.resume(returning: array)
}
}
}
}
If you aren't calling continuation there is no point in returning a continuation of any kind.
I'm writing a chat application using firebase , I notice memory leak in ChatView while observing database changes which is when a message is sent or received.
when I comment out the database observation the memory leak dose not happen anymore so I'm guessing this is a firebase problem .
I'm sharing the code so please if you know what is acutely causing the memory leak help me out.
ChatViewModel:
class ChatViewModel : ObservableObject {
/// - sub ViewModels :
#Published private(set) var messages : [MessageModel] = []
private(set) var conversationID : String? = nil
/// set shared conversationID
/// - Parameter convesationID: shared conversationID if exist
func setConverationID(convesationID : String?){
guard let convesationID = convesationID else {
print("CONVERSATION ID DOUS NOT EXIT")
return
}
self.conversationID = convesationID
startObservingConversation()
}
/// start observing the conversation with viewModel conversationID
private func startObservingConversation(){
guard let conversationID = self.conversationID else {
return
}
DatabaseManager.shared.observeMessagesForConversation(conversationId: conversationID) { [weak self] message in
self?.messages += message
}
}}
ChatView :
struct ChatView: View {
#StateObject var viewModel = ChatViewModel()
var body: some View {
VStack(alignment : .leading , spacing: 0){
ScrollViewReader { scrollViewReader in
List{
ForEach(viewModel.messages) { item in
MessageView(messsage: item.text)
.id(item.id)
}
}
}
}
}}
observerMessages :
func observeMessagesForConversation(conversationId id :String,compelition : #escaping ([MessageModel]) -> Void ) {
database.child(id).child("messages").observe(.childAdded) { snapshot in
guard let value = snapshot.value as? [String:Any] else {
compelition([])
return
}
var messages : [MessageModel] = []
let decoder = JSONDecoder()
guard
let jsonData = try? JSONSerialization.data(withJSONObject:value),
let message = try? decoder.decode(MessageModel.self, from: jsonData) else {
compelition([])
return
}
messages.append(message)
compelition(messages)
}
}
However, getting an error message that says that my class 'Expenses' does not conform to protocol 'Decodable' & Type 'Expenses' does not conform to protocol 'Encodable'
import Foundation
class Expenses : ObservableObject, Codable {
#Published var items : [ExpenseItem] {
// Step 1 creat did set on publsihed var.
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {
UserDefaults.standard.set(encoded, forKey: "Items")
}
}
}
init() {
if let items = UserDefaults.standard.data(forKey: "Items") {
let decoder = JSONDecoder(
if let decoded = try?
decoder.decode([ExpenseItem].self, from: items) {
self.items = decoded
return
}
}
self.items = []
}
}
my expense item is flagged as
struct ExpenseItem : Identifiable, Codable {
let id = UUID()
let name : String
let type : String
let amount : Int
}
Conformance to Encodable/Decodable is auto-synthesized when all stored properties conform to Encodable/Decodable, but using a property wrapper on a property means that now the property wrapper type needs to conform to Encodable/Decodable.
#Published property wrapper doesn't conform. It would have been nice to just implement conformance on the Published type itself, but unfortunately it doesn't expose the wrapped value, so without using reflection (I've seen suggestions online), I don't think it's possible.
You'd need to implement the conformance manually:
class Expenses : ObservableObject {
#Published var items : [ExpenseItem]
// ... rest of your code
}
extension Expense: Codable {
enum CodingKeys: CodingKey {
case items
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.items, forKey: .items)
}
required init(from decoder: Decoder) throws {
var container = try decoder.container(keyedBy: CodingKeys.self)
self.items = try container.decode([ExpenseItem].self, forKey: .items)
}
}
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()
}
}
API call and JSON decoding are working fine, as I can print to console any item from the JSON data set without a problem.
Here's API call and test print:
import Foundation
import SwiftUI
import Combine
class APICall : ObservableObject {
#Published var summary: Summary?
init () {
pullSummary()
}
func pullSummary() {
let urlCall = URL(string: "https://api.covid19api.com/summary")
guard urlCall != nil else {
print("Error reaching API")
return
}
let session = URLSession.shared
let dataTask = session.dataTask(with: urlCall!) { (data, response, error) in
if error == nil && data != nil {
let decoder = JSONDecoder()
do {
let summary = try decoder.decode(Summary.self, from: data!)
print(summary.byCountry[40].cntry as Any)
DispatchQueue.main.async {
self.summary = summary
}
}
catch {
print("Server busy, try again in 5 min.")
}
}
}
dataTask.resume()
}
}
And here is the structure of the "Summary" data model used for the decoding and data object structure:
import Foundation
struct Summary: Decodable {
let global: Global
let byCountry: [ByCountry]
let date: String
enum CodingKeys: String, CodingKey {
case global = "Global"
case byCountry = "Countries"
case date = "Date"
}
struct Global: Decodable {
let globalNC: Int
let globalTC: Int
let globalND: Int
let globalTD: Int
let globalNR: Int
let globalTR: Int
enum CodingKeys: String, CodingKey {
case globalNC = "NewConfirmed"
case globalTC = "TotalConfirmed"
case globalND = "NewDeaths"
case globalTD = "TotalDeaths"
case globalNR = "NewRecovered"
case globalTR = "TotalRecovered"
}
}
struct ByCountry: Decodable {
let cntry: String?
let ccode: String
let slug: String
let cntryNC: Int
let cntryTC: Int
let cntryND: Int
let cntryTD: Int
let cntryNR: Int
let cntryTR: Int
let date: String
enum CodingKeys: String, CodingKey {
case cntry = "Country"
case ccode = "CountryCode"
case slug = "Slug"
case cntryNC = "NewConfirmed"
case cntryTC = "TotalConfirmed"
case cntryND = "NewDeaths"
case cntryTD = "TotalDeaths"
case cntryNR = "NewRecovered"
case cntryTR = "TotalRecovered"
case date = "Date"
}
}
}
As shown, the results of the API call and JSON decode are published as required using ObserveableObject and #Published.
Over in the ContentView, I have followed the ObservedObject rules and only want to display on the UI a data point from the JSON data to confirm it's working:
import SwiftUI
import Foundation
import Combine
struct ContentView: View {
#ObservedObject var summary = APICall()
var body: some View {
Text($summary.date)
.onAppear {
self.summary.pullSummary()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
BUT... I get these 2 errors at the Text display line, 1) Initializer 'init(_:)' requires that 'Binding<Subject>' conform to 'StringProtocol' and 2) Value of type 'ObservedObject<APICall>.Wrapper' has no dynamic member 'date' using the key path from root type 'APICall'.
I am guessing the 2nd error is the root cause of the problem, indicating the data is not being passed into the ContentView correctly.
I appreciate any suggestions.
Thanks.
It is messed view model with internal property
struct ContentView: View {
#ObservedObject var viewModel = APICall()
var body: some View {
Text(viewModel.summary?.date ?? "Loading...") // << no $ sign !!!
.onAppear {
self.viewModel.pullSummary()
}
}
}