Implementing Login Navigation in SwiftUI - swiftui

Here is my requirement: I want to display a login screen, perform authentication via a network request, and if successful, display the content page.
I have implemented the network request part as per the suggestion by jpdnx here.
I have a UserAuth class and the corresponding code to display the Login View if not logged in, and the Content View if logged in, as per M Reza here.
However, I am not able to combine the two - when the network request finishes in LoginView, I am not able to get the flow back to the LoginControllerView to navigate to ContentView. Any help will be appreciate here.
Here is my code:
Network.swift
class ViewModel<T: Codable> : ObservableObject {
//#Published var calendar : IDCCalendar?
#Published var modelData : T?
func getData(url: URL, encoded: String, completion: (#escaping ()->()) ) {
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
print(encoded)
let encoder = JSONEncoder()
if let data = try? encoder.encode(encoded) {
request.httpBody = data
}
print("Request: \(request)")
URLSession.shared.dataTask(with: request) { data1, response, error in
if let error = error {
print("Request error: ", error)
return
}
guard let data1 = data1 else { return }
DispatchQueue.main.async {
let decoder = JSONDecoder()
print(String(decoding: data1, as: UTF8.self))
if let value = try? decoder.decode(T.self, from: data1)
{
self.modelData = value
completion()
print("Success!")
}
else {
print("Does not decode correctly")
}
}
}.resume()
}
LoginControllerView.swift
var body: some View {
Group {
if !userAuth.isLoggedin {
LoginView(userAuth: userAuth)
} else {
ContentView()
}
}
LoginView.swift
struct LoginView: View {
#EnvironmentObject var userAuth: UserAuth
#StateObject var viewModel = ViewModel<Login>()
var body: some View {
// Login Screen UI here
Button("Login") {
// Commenting out network request code out for now
/*
let url = URL(<URL goes here>)!
let encoded = <JSON String goes here>
let completion = {
if viewModel.modelData?.Status == 0 {
userAuth.isLoggedin = true
let login = viewModel.modelData!
let encoder = JSONEncoder()
if let data = try? encoder.encode(login) {
UserDefaults.standard.set(data, forKey: "LoginData")
}
}
viewModel.getData(url: url, encoded: encoded, completion: completion)
*/
// For test purposes, setting it to true, I want this to percolate back to
// LoginControllerView
userAuth.isLoggedin = true
}
UserAuth.swift
import Combine
class UserAuth: ObservableObject {
let didChange = PassthroughSubject<UserAuth,Never>()
// required to conform to protocol 'ObservableObject'
let willChange = PassthroughSubject<UserAuth,Never>()
func login() {
// login request... on success:
self.isLoggedin = true
}
var isLoggedin = false {
didSet {
didChange.send(self)
}
}
}

Related

Encode and decode swiftui MKPlacemark

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.

URLSession HTTP Error not updating back in view unless the action is initiated again

I have a view with a button that calls an API, the API either returns an HTTP code 200 or 400 based on a particular scenario.
The button works just fine and everything works smoothly if code 200 is returned, however if code 400 is returned, the view is not updated that the user have to click on the button once again to get the updated message.
I added the http code property as a published variable in the VM's class and the http is an observable, but it doesn't get updated in the view on the first API call, I'm not sure what I'm missing.
I made a lot of changes to the shared code just to help in demonstrating the actual problem.
Update: Also I think another part of the problem, is that the url function returns the value before the url session returns the data, I don't know why this is happening, that when I execute it a second time it uses the values from the previous execution.
HTTPError Class
class HTTPError : Codable, ObservableObject {
var statusCode: Int?
var message: [String]?
var error: String?
init(statusCode: Int? = nil, message: [String]? = [], error: String? = nil){
self.statusCode = statusCode
self.message = message ?? []
self.error = error
}
convenience required init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.statusCode = try container.decodeIfPresent(Int.self, forKey: .statusCode)
do {
self.message = try container.decodeIfPresent([String].self, forKey: .message)
} catch {
guard let value = try container.decodeIfPresent(String.self, forKey:
.message) else {return}
self.message = []
self.message?.append(value)
}
self.error = try container.decodeIfPresent(String.self, forKey: .error)
}
VM Class
class VM: ObservableObject {
#Published var isLoading = true
#Published var httpError = HTTPError()
func checkDriverIn(_ record: DriverQRParam) async -> (Bool) {
...
var request = URLRequest(url: url)
...
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
do {
...
let task = URLSession.shared.dataTask(with: request) { (data, response,
error) in
guard let data = data, error == nil else {
print(error ?? "Unknown error")
return
}
self.httpError = try! JSONDecoder().decode(HTTPError.self, from: data)
//gets updated just fine in this class//
}
task.resume()
}catch {
print("Couldn't encode data \(String(describing: error))")
}
if httpError.statusCode != nil && httpError.statusCode == 400 {
return (false)
} else {
return (true)
}
}
View.Swift
struct xyz: View {
#State private var VM = VM()
Button("click") {
Task {
await VM.checkDriverIn(driverParam)
}
}
}

How can I declare the Json variable in this case on swiftUI

It cannot show the result, anyone knows?
struct Course: Hashable, Codable {
let id:String
let display:String
}
class ViewModel: ObservableObject{
#Published var courses: [Course]= []
func fetch(){
guard let url = URL(string:"https://www.i-design.hk/api/menu/userMenuRequest.php?type=userMenu&action=l&userId=200380")else{
return
}
let task = URLSession.shared.dataTask(with: url) { [weak self]data,_, error in
guard let data = data, error == nil else {
return
}
I have tried many times to change the code, but I cannot show the JSON string correctly. Anyone can help me?
when the response came from API it came in form of JSON and you need to decode that JSON so that can easily read by swift, so you should decode the response like that and when you get the response you should assign the data to the courses variable so that it will published to the view
struct Course: Hashable, Codable {
let id:String
let display:String
}
class ViewModel: ObservableObject{
#Published var courses: [Course]= []
#Published var error: String = ""
func fetch(){
guard let url = URL(string:"https://www.i-
design.hk/api/menu/userMenuRequest.php?
type=userMenu&action=l&userId=200380")else{
return
}
let task = URLSession.shared.dataTask(with: url) { [weak
self]data,_, error in
guard let data = data, error == nil else {
return
}
do{
let ResponseResult = try JSONDecoder().decode([Course], from: data)
courses = ResponseResult
}catch(let error){
error = error.localizedDescription
}
}

Memory Leak with swiftUI List and Firebase database observing childAdded

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)
}
}

Swift How to reload an Environment-Object

I want to add a Button to my view to load more data. In my Environment Object the data is generated randomly via an API.
How can I reload my Environment object to get new items. The Code Below should make it clear. Thanks in advance
class observer : ObservableObject{
#Published var shows = [stacks]()
#Published var last = -1
var results = [Result1]()
init(){
let number = Int.random(in: 1...35)
print("das ist dier etste radodm nube: \(number)")
let endnumber = number + 8
print("das ist dier etste radodm nube: \(endnumber)")
for n in number...endnumber{
guard let url = URL(string:"https://api.themoviedb.org/3/discover/tv?api_key=3ed2bd15f916d0e3fbb77c193bf33b61&language=de-DE&region=DE&with_networks=213&page=\(n)" ) else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
DispatchQueue.main.async {
self.results = decodedResponse.results
for i in self.results{
self.shows.append(stacks(id: "\(i.id)", name: i.name, typ: "Serie", status: "",overview: i.overview, vote: "\(i.vote_average)", image: i.poster_path, swipe: 0, degree: 0, commercal: "no"))
}
self.shows.shuffle()
}
return
}
}
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}
}
struct View: View {
#EnvironmentObject var cards : observer
var body: some View {
VStack{
Button(action: {
//reload
}){
Text("reload")
}
}
I would put the fetching/loading data code that is inside the init in a function.
func codeToFetchData() {
//Code from your init
}
The call that method in your init and your Button for example cards.codeToFetchData()