Load Various Custom Views inside 'some View ' - ios

I've been working on a SwiftUI project that generates UIs based on Server responses using JSON
In order to do that I have created a FormBuilder
Now I want to show various types of custom fields like TextField, DateField, TextArea
all fields have
text, title, etc as common properties but will have different Validating procedures
in order to program this,
I have used a protocol name "FormField", "SectionView" struct to load fields,
some structs as Fields comforting to FormField
and "FieldView" struct to load different Fields.
In the FieldView protocol I'm getting following error when I try to show various Views based on the type I get via json
I Have commented the line that shows the error in FieldView class
Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type
Can anyone help me to find a work around with this
Any help will be so much appreciated !
FormField Protocol
protocol FormField : View {
var id : String! { get set }
var title : String? { get set }
var placeholder : String? { get set }
var text : String? { get set }
var validation : String? { get set }
var keyboardType : String? { get set }
init(json : JSON)
func validate()
func showError()
func isValid() -> Bool
}
FieldView struct
Have commented the line that shows the error
struct FieldView: View {
private let TEXT = "text"
private let TEXTAREA = "textarea"
private let RADIO = "radio"
// could have more type
let id = UUID().uuidString
private var type : String!
private let fieldJson : JSON
init(json : JSON) {
self.type = json["type"].stringValue
self.fieldJson = json
}
var body: some View {
field
}
private var field : some FormField{
// Errors Comes from above Line
if type == TEXT {
FormTextField(json: fieldJson)
}
if type == TEXTAREA {
FormTextArea(json: fieldJson)
}
if type == RADIO {
FormRadio(json: fieldJson)
}
}
func validate() {
field.validate()
}
func showError() {
field.showError()
}
func isValid() -> Bool{
return field.isValid()
}
}
FormTextField struct
struct FormTextField: FormField {
var id : String!
var title : String?
var placeholder : String?
#State var text : String?
var validation : String?
var keyboardType : String?
init(json: JSON) {
self.id = json["id"].string
self.title = json["name"].string
self.placeholder = json["placeholder"].string
self.text = json["text"].string
self.validation = json["validation"].string
self.keyboardType = json["keyboardType"].string
}
var body: some View {
Text("Text Field: \(title!)")
.font(.headline)
.padding()
.background(Color.red)
}
func validate() {
if title!.isEmpty {
print("FormTextField Error")
}
}
func showError() {
}
func isValid() -> Bool{
return title!.isEmpty && text!.isEmpty
}
}
FormTextArea struct
struct FormTextArea: FormField {
var id : String!
var title : String?
var placeholder : String?
#State var text : String?
var validation : String?
var keyboardType : String?
init(json: JSON) {
self.id = json["id"].string
self.title = json["name"].string
self.placeholder = json["placeholder"].string
self.text = json["text"].string
self.validation = json["validation"].string
self.keyboardType = json["keyboardType"].string
}
var body: some View {
Text("Text Area: \(title!)")
.font(.headline)
.padding()
.background(Color.red)
}
func validate() {
print("Form Text Area")
}
func showError() {
}
func isValid() -> Bool{
return title!.isEmpty
}
}
FormRadio and other Fields also as same as this
Following SectionView struct was used to add fields inside of a VStack
SectionView struct
struct SectionView: View {
var id = UUID().uuidString
public var title : String?
public var fields = [FieldView]()
init(json : JSON) {
self.title = json["sectionName"].string
fields = (json["fields"].array ?? []).map({return FieldView.init(json: $0)})
}
var body: some View {
VStack{
Text(title ?? "Section")
.font(.title)
ForEach(fields,id:\.id){field in
field
}
}
.padding()
}
}

Related

Firebase is not saving the data - swiftui

I have a problem that Firebase not saving the data and it shows like that :
enter image description here
First I created a model :
struct Box: Identifiable, Hashable, Codable {
#DocumentID var id: String?
var boxName: String
var boxSize: String
enum CodingKeys: String, CodingKey {
case id
case boxName
case boxSize
}
var dictionary: [String: Any] {
let data = (try? JSONEncoder().encode(self)) ?? Data()
return (try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]) ?? [:]
}
}
Then I created ViewModel:
class BoxViewModel: ObservableObject {
#Published var box: Box
private var db = Firestore.firestore()
init(box: Box = CashBox(boxName: "", boxSize: "")) {
self.box = box
}
private func addNewBox(_ box: Box){
do {
print("the name \(box.boxName)")
let _ = db.collection("Box")
.addDocument(data: box.dictionary)
}
}
func addBox() {
self.addNewBox(box)
}
func setBoxData(boxName: String, boxSize: String){
self.box.boxName = boxName
self.box.boxSize = boxSize
}
}
Finally here is the view:
struct CashBoxView: View {
#State var boxName = ""
#State var boxSize = ""
#EnvironmentObject var boxViewModel: BoxViewModel
var body: some View {
Text("Enter box name")
TextField("", text: $boxName)
Text("Enter box size")
TextField("", text: $boxSize)
Button( action: {
boxViewModel.setBoxData(boxName: boxName, boxSize: boxSize)
boxViewModel.addBox()
}) {
Text("Done")
}
}
First I thought that the problem is the box is empty but when I tried to print the box name print("the name \(box.boxName)") and printed it
is the problem the the viewModel is #EnvironmentObject ? or what is the problem ?
Thank you,

NavigationLink doesn't work in nested ScrollView after EnvironmentObject update from network request

I have two tabs for navigationView, Dashboard and Userscreen.
Userscreen is used for fetching user information through network request, and dashboard is showing information through network request based on user information, hense user information is the environment object here.
After update user information I can view dashboard properties but click on navigation link is not working.
After doing some testing, I found that the navigation link(showing based on user info) under ScrollView -> ScrollView(.horizontal) -> NavigationLink is not working, but navigation link(showing based on user info) under VStack -> ScrollView(.horizontal) -> NavigationLink performs well.
I wonder if I was missing something on the implementation or may I ask is it an existing bug for XCode?
Edit: Updated code
Edit again: Please refer to #workingdog solution below, the whole problem is just caused by a simple silly mistake, which is not declare the right state/environment object.
import SwiftUI
import Kingfisher
import Alamofire
struct ExampleView: View {
#StateObject var userInfoManager = UserInfoManagerExample()
var body: some View {
NavigationView {
TabView (){
DashboardScreenExample()
.environmentObject(userInfoManager)
.tabItem{
Text("Dashboard")
}
LoginScreenExample()
.environmentObject(userInfoManager)
.tabItem{
Text("UpdateInfo")
}
}
.navigationBarTitleDisplayMode(.inline)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DashboardScreenExample: View {
#EnvironmentObject var userInfoManager : UserInfoManagerExample
#State var please : Bool = false
init(){
Theme.navigationBarColors(background: UIColor(viewProperties.themered), titleColor: .white)
}
var viewProperties = ViewProperties()
var body: some View {
if(userInfoManager.shouldShowDashboard){
ScrollView{//Switch this ScrollView to VStack make Navigation Link work again
//I had other screens too, but screen below is the one causing problem
DashboardContentExample()
}
}else{
ProgressView().onAppear{userInfoManager.shouldShowDashboard = true}
}
}
}
struct DashboardContentExample: View {
#EnvironmentObject var userInfoManager : UserInfoManagerExample
#ObservedObject var dashboardList = DashboardConfigRequestExample()
func loadRequest(){
dashboardList.loadData(passedLanguage: userInfoManager.preferenceLangauge)
}
//View start from here
var body: some View {
if(dashboardList.dashboardConfig.count == 0){
ProgressView()
.onAppear{
loadRequest()
}
}else{
ScrollView(.horizontal){
LazyHStack{
//Here is the NavigationLink not working
NavigationLink(destination: EmptyView()){
Text("Hello")
}
}
}
}
}
}
struct LoginScreenExample: View {
let usernameLoginRequest = UsernameLoginRequestExample()
#EnvironmentObject var userInfoManager : UserInfoManagerExample
#State var loginClicked : Bool = false
var body: some View {
VStack(alignment: .leading){
if(userInfoManager.isLogin){
Text("You have logged in.")
.onAppear{
loginClicked = false
}
}
else{
ZStack{
VStack{
Spacer()
//the username and password had to be entered
Button(action:{clickLogin(forUsername: "iamusername", andPassword: "iampassword")}){
Text("Sign In")
}
Spacer()
}
if(loginClicked){
ProgressView()
}
}
}
}
}
func clickLogin(forUsername: String, andPassword: String){
loginClicked.toggle()
usernameLoginRequest.loadData(forUsername: forUsername, andPassword: andPassword, andUserInfo: userInfoManager)
}
}
class UserInfoManagerExample : ObservableObject{
#Published var isLogin: Bool = false
#Published var username: String = ""
#Published var password: String = ""
#Published var preferenceLangauge: String = "en"
#Published var shouldShowDashboard = true
init(){
}
func updateCredential(forUsername: String, andPassword: String){
username = forUsername
password = andPassword
}
}
struct UsernameLoginRequestExample{
func loadData(forUsername: String, andPassword: String, andUserInfo: UserInfoManagerExample){
let request_url = "This is the request url"
let request_parameters = getUsernameLoginParameter(withUsername: forUsername, andPassword: andPassword)
AF
.request(request_url, method: .post, parameters: request_parameters, encoding: JSONEncoding.default ).responseJSON{
responses
in
debugPrint(responses)
switch responses.result{
case .success:
do {
andUserInfo.shouldShowDashboard = false
andUserInfo.updateCredential(forUsername: forUsername, andPassword: andPassword)
andUserInfo.isLogin = true
}
case let .failure(error):
print(error)
}
}
}
func getUsernameLoginParameter(withUsername : String, andPassword : String) -> [String : String]{
var parameters : [String : String] = [String : String]()
parameters.updateValue(withUsername, forKey: "username")
parameters.updateValue(andPassword, forKey: "password")
var device_id : String
if(UIDevice.current.identifierForVendor != nil){
device_id = UIDevice.current.identifierForVendor!.uuidString
}else{
device_id = ""
}
parameters.updateValue(device_id, forKey:"device_id")
return parameters
}
}
class DashboardConfigRequestExample : ObservableObject{
#Published var dashboardConfig = [DashboardConfigExample]()
func loadData(passedLanguage : String){
let request_url = "This is request url"
let request_parameters = getDashboardConfigParameter(passedLanguage: passedLanguage)
AF
.request(request_url, method:.post, parameters: request_parameters)
.responseJSON{
responses in
switch responses.result {
case .success:
guard
let data = responses.data
else { return }
do {
let configResponse = try JSONDecoder().decode(DashboardConfigDataExample.self, from: data)
self.dashboardConfig = configResponse.list ?? []
} catch {
print(error)
}
case let .failure(error):
print(error)
}
}
}
func getDashboardConfigParameter(passedLanguage: String) -> [String:String]{
var parameters : [String:String] = [String:String]()
parameters.updateValue(passedLanguage, forKey: "language")
return parameters
}
}
struct DashboardConfigDataExample : Decodable{
public var status: String?
public var list: [DashboardConfigExample]?
}
struct DashboardConfigExample: Encodable & Codable {
let background_image: String?
let background_color: String?
}
Try replacing
#ObservedObject var infoRequest = InfoRequest()
in Dashboard with
#StateObject var infoRequest = InfoRequest()
EDIT1: in light of the new code.
Try moving #ObservedObject var dashboardList = DashboardConfigRequestExample() of DashboardContentExample
into ExampleView as #StateObject. Something like the following example code:
struct ExampleView: View {
#StateObject var userInfoManager = UserInfoManagerExample()
#StateObject var dashboardList = DashboardConfigRequestExample() // <-- here
var body: some View {
NavigationView {
TabView (){
DashboardScreenExample().tabItem{ Text("Dashboard")}
LoginScreenExample().tabItem{ Text("UpdateInfo")}
}.navigationBarTitleDisplayMode(.inline)
}
.environmentObject(userInfoManager) // <-- here
.environmentObject(dashboardList) // <-- here
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DashboardContentExample: View {
#EnvironmentObject var userInfoManager : UserInfoManagerExample
#EnvironmentObject var dashboardList: DashboardConfigRequestExample // <-- here
func loadRequest(){
dashboardList.loadData(passedLanguage: userInfoManager.preferenceLangauge)
}
//View start from here
var body: some View {
if(dashboardList.dashboardConfig.count == 0){
ProgressView()
.onAppear{
loadRequest()
}
}else{
ScrollView(.horizontal){
LazyHStack{
//Here is the NavigationLink not working
NavigationLink(destination: Text("Hello destination")){ // <-- here for testing
Text("Hello")
}
}
}
}
}
}

Why my published var is not being updated by my view?

I am making a personal project to study SwiftUI. All was going well, the I noticed a bug on my app.
I have the simple view bellow, that saves a description, a value and some tags on my ViewModel. I am having an issue with the $viewModel.value. That variable is not being filled with values from the view.
I supose that my #Published var value: Double? from my ViewModel should be updated whenever the user types some value. Thing is, it is not updating on any iPhone 11 and up, but it works perfectly on the iPhone 8.
public struct AddBillView: View {
#ObservedObject private var viewModel: AddBillViewModel
#Environment(\.presentationMode) var presentationMode
public let onExpenseCreated: ((_ expense: Expense)->Void)
public var body: some View {
Text("Add Expense")
VStack {
TextField("Descrição", text: $viewModel.name)
HStack {
Text("Valor \(NumberFormatter.currency.currencySymbol)")
CurrencyTextField("Value", value: $viewModel.value)
.multilineTextAlignment(TextAlignment.leading)
}
HStack {
Text("Tags")
TextField("car pets home",
text: $viewModel.tags)
}
Picker("Type", selection: $viewModel.type) {
Text("Paid").tag("Paid")
Text("Unpaid").tag("Unpaid")
Text("Credit").tag("Credit")
}
}.navigationTitle("+ Expense")
Button("Adicionar") {
if !viewModel.hasExpense() {
return
}
onExpenseCreated(viewModel.expense())
self.presentationMode.wrappedValue.dismiss()
}
}
public init(viewModel outViewModel: AddBillViewModel,
onExpenseCreated: #escaping ((_ expense: Expense)->Void)) {
self.viewModel = outViewModel
self.onExpenseCreated = onExpenseCreated
}
}
And I have a ViewModel:
public class AddBillViewModel: ObservableObject {
#Published var name: String = ""
#Published var type: String = "Paid"
#Published var tags: String = ""
#Published var value: Double?
init(expense: Expense?=nil) {
self.name = expense?.name ?? ""
self.type = expense?.type.rawValue ?? "Paid"
self.tags = expense?.tags?.map { String($0.name) }.joined(separator: " ") ?? ""
self.value = expense?.value
}
func hasExpense() -> Bool {
if self.name.isEmpty ||
self.value == nil ||
self.value?.isZero == true {
return false
}
return true
}
func expense() -> Expense {
let tags = self.tags.split(separator: " ").map { Tag(name: String($0)) }
return Expense(name: self.name, value: self.value ?? 0.0 ,
type: ExpenseType(rawValue: self.type)!,
id: UUID().uuidString,
tags: tags)
}
}
Then I use my view:
AddBillView(viewModel: AddBillViewModel()) { expense in
viewModel.add(expense: expense)
viewModel.state = .idle
}
I already google it and spend a couple of hours looking for an answer, with no luck. Someone have any ideas?
Edited
Here is the code for the CurrencyTextField. I`m using this component:
https://github.com/youjinp/SwiftUIKit/blob/master/Sources/SwiftUIKit/views/CurrencyTextField.swift
But the component works perfectly fine on iPhone 8 simulator and with a #State property inside my view. It does not work only with my ViewModel
I figured it out! The problem was that my AddBillViewModel is an ObservableObject and I was marking each property with #Published. This was causing some kind of double observable object.
I removed the #Published and it started working again.

Passing #Binding variable from protocol var to if in SwiftUI

I have a model like this:
protocol PurchasableProduct {
var randomId: String { get }
}
class Cart: Identifiable {
var items: [PurchasableProduct]
init(items: [PurchasableProduct]) {
self.items = items
}
}
class Product: Identifiable, PurchasableProduct {
var randomId = UUID().uuidString
var notes: String = ""
}
class DigitalGood: Identifiable, PurchasableProduct {
var randomId = UUID().uuidString
}
where items conform to protocol PurchasableProduct.
I want to build a View that shows cart like this:
struct CartView: View {
#State var cart: Cart
var body: some View {
List {
ForEach(cart.items.indices) { index in
CartItemView(item: self.$cart.items[index])
}
}
}
}
where CartItemView is:
struct CartItemView: View {
#Binding var item: PurchasableProduct
var body: some View {
VStack {
if self.item is Product {
Text("Product")
} else {
Text("Digital Good")
}
}
}
}
That's working and give me result as
This (screenshot)
But I want to extend this a but more that my items element can be passed as a binding variable lets say as:
struct CartItemView: View {
#Binding var item: PurchasableProduct
var body: some View {
VStack {
if self.item is Product {
VStack {
TextField("add notes", text: (self.$item as! Product).notes) // ❌ Cannot convert value of type 'String' to expected argument type 'Binding<String>'
TextField("add notes", text: (self.$item as! Binding<Product>).notes) // ⚠️ Cast from 'Binding<PurchasableProduct>' to unrelated type 'Binding<Product>' always fails
}
} else {
Text("Digital Good")
}
}
}
}
What I'm trying to achieve is:
I have a collection of items that depends on a class should be drawn differently
Items have different editable sync that should be binded into CartView
Not sure if thats syntax issue or my approach issue ... how to cast this on body to get the correct view based on type?
You may create a custom binding:
struct CartItemView: View {
#Binding var item: PurchasableProduct
var product: Binding<Product>? {
guard item is Product else { return nil }
return .init(
get: {
self.$item.wrappedValue as! Product
}, set: {
self.$item.wrappedValue = $0
}
)
}
var body: some View {
VStack {
if product != nil {
TextField("add notes", text: product!.notes)
} else {
Text("Digital Good")
}
}
}
}

pass values dynamically for network request

I have to pass the value of movie.id which is received from a View which is called ReviewView.
I need to pass the movie.id value received in this view to ReviewFetcher and then make a network request using that movie.id. As of now I have hard coded the movie id in ReviewFetcher but I require this to be received from ReviewView and then make a request and then update the list in ReviewView.
Below is the Code:-
ReviewFetcher.swift
import Foundation
import Alamofire
import SwiftUI
class ReviewObserver: ObservableObject {
#Published var review = ReviewArray(id: 1, page: 9, results: [])
// #State var movieID:Int
init() {
// self.movieID = movieID
getReviews(movieID : 181812)
}
func getReviews(movieID:Int) {
//self.review.results.removeAll()
let reviewURL = "https://api.themoviedb.org/3/movie/"+String(movieID)+"/reviews?api_key=a18f578d774935ef9f0453d7d5fa11ae&language=en-US&page=1"
Alamofire.request(reviewURL)
.responseJSON { response in
if let json = response.result.value {
if (json as? [String : AnyObject]) != nil {
if let dictionaryArray = json as? Dictionary<String, AnyObject?> {
let json = dictionaryArray
if let id = json["id"] as? Int,
let page = json["page"] as? Int,
let results = json["results"] as? Array<Dictionary<String, AnyObject?>> {
for i in 0..<results.count {
if let author = results[i]["author"] as? String,
let content = results[i]["content"] as? String,
let url = results[i]["url"] as? String {
let newReview = ReviewModel(author: author,
content: content,
url: url)
self.review.results.append(newReview)
}
}
}
}
}
}
}
}
}
ReviewView.swift
import SwiftUI
struct ReviewsView: View {
#State var movie: MovieModel
#Binding var reviews:[ReviewModel]
#ObservedObject var fetcher = ReviewObserver()
var body: some View {
VStack(alignment:.leading) {
Text("Review")
.font(.largeTitle)
.bold()
.foregroundColor(Color.steam_rust)
.padding(.leading)
Divider()
// Text(String(fetcher.movieID))
List(fetcher.review.results) { item in
VStack(alignment:.leading) {
Text("Written by : "+item.author)
.font(.body)
.bold()
.padding(.bottom)
Text(item.content)
.font(.body)
.lineLimit(.max)
}
}
}
}
}
MovieModel.swift
import Foundation
import SwiftUI
import Combine
struct MovieArray: Codable {
var page: Int = 0
var total_results: Int = 0
var total_pages: Int = 0
var results: [MovieModel] = []
}
struct MovieModel: Codable, Identifiable {
var id : Int
var original_title: String
var title: String
var original_language:String
var overview: String
var poster_path: String?
var backdrop_path: String?
var popularity: Double
var vote_average: Double
var vote_count: Int
var video: Bool
var adult: Bool
var release_date: String?
}
Remove the init() of your ReviewObserver class. and then call getReviews method in .onAppear modifier of your VStack. The idea of what you need:
class ReviewObserver: ObservableObject {
#Published var review = ReviewArray(id: 1, page: 9, results: [])
func getReviews(movieID:Int) {
//you block,, anything you wanna do with movieID.
//Assume you are going to change 'review' variable
}
}
struct ReviewsView: View {
#State var movie:MovieModel
#Binding var reviews:[ReviewModel]
#ObservedObject var fetcher = ReviewObserver()
var body: some View {
VStack(alignment:.leading){
Text("Review")
Divider()
Text(String(fetcher.movieID))
List(fetcher.review.results)
{
item in
VStack(alignment:.leading){
Text("Written by : "+item.author)
}
}.onAppear {
self.fetcher.getReviews(movieID: movie.id)
}
}
}

Resources