Apologies beforehand if this already has an answer but I wasn't able to find the answer that I was looking for.
I have been stuck on the best way to approach passing data from the UIView to the UIViewController. Let's suppose I have this form data with information the user has filled out. The data exists in the view via the individual UITextFields. How should I pass these information to the controller to perform validation and to create a post request with this data?
Does it make sense to do this via a closure? Like the following:
#objc func submitFormData() {
// call function passed via the view controller
}
What is the best practises for passing data between the view and the controller? For your information, I am not using StoryBoard and I am creating everything programmatically.
Answers would be greatly appreciated, thanks!
First, make a structure/Model of your data and do needfull validation.
class UserDataSpecifier {
var fields = [UserField]()
struct UserField {
var title: String?
var placeHolder: String = ""
var inputTxt: String = ""
var image: String = ""
init(titleStr: String, inputStr: String? = "", img: String = "") {
title = titleStr
placeHolder = titleStr
inputTxt = inputStr ?? ""
image = img
}
}
init() {
prepareForSignup()
}
func prepareForSignup() {
fields.append(UserField(titleStr: "First Name")
fields.append(UserField(titleStr: "Email"))
fields.append(UserField(titleStr: "Password"))
}
func isValidData(type: FormType) -> (isValid: Bool, error: String) {
if fields[0].inputTxt.isEmpty {
return (false, "Enter your message")
} else if fields[1].inputTxt.isEmpty {
return (false, "Enter your email")
}
return (true, "")
}
}
In your ViewController class make an instance of data. Fill the value of that object and then validate it.
var userData: UserDataSpecifier = UserDataSpecifier()
userData.fields[0].inputTxt = "name"
userData.fields[1].inputTxt = "email#gmail.com
let result = userData.isValidData()
if result.isValid {
print("Valid data")
} else {
print(result.error)
}
You can also pass this userData instance to your view and fill your data from your View. After filling data validate it in ViewController.
In my opinion the best way to pass data from the View to the ViewController would be via Delegates and Protocols.
Create a protocol in the ViewController with the submitFormData() function.
Declare a delegate variable in the view
Set the ViewController as the View's delegate
Then in your "submitFormData" function, call the delegate.submitFormData().
There are also other ways to pass data, this is just my personal preference. Hope this helped !
Related
I have an app that uses the UITabBarController and I have a model which I use to pass information between the various Tabs
class BaseTBController: UITabBarController {
var title: String = ""
var valueData = Double()
override func viewDidLoad() {
super.viewDidLoad()
}
Normally within any of the Tabs in the TabBars, I can just do:
let tabbar = tabBarController as! BaseTBController
let valueData = Int(tabbar.valueData) == 0 ? Int(UserDefaults.standard.double(forKey: UDKeys.valueData)) : Int(tabbar.valueData)
Now, the situation I'm in is like this, I would like to use the data from the Tabbar model data in a helper function (as struct)
Doing this, it doesn't work. Wondering if there is a way to do this and my googling/SO searching skills are just not up to par. TX
struct DataFieldOptions{
static func showValueAs() -> String {
let tabbar = tabBarController as! BaseTBController
let valueData = Int(tabbar.valueData) == 0 ? Int(UserDefaults.standard.double(forKey: UDKeys.valueData)) : Int(tabbar.valueData)
return String(valueData)
}
This seems a bit of an odd approach. If you are adding a "helper" I would expect you'd have a data-manager class rather than using a var in the tab bar controller.
However, tabBarController is an Optional property of a view controller. If you want to access it from outside the view controller you need to give it a reference.
As a side note, you're showing -> String but you're returning an Int ... I'm going to guess you're really planning on formatting the value as a string in some way, so here's how to do it returning a string:
struct DataFieldOptions{
static func showValueAs(vcRef vc: UIViewController) -> String {
// safely make sure we have access to a tabBarController
// AND that it is actually a BaseTBController
if let tbc = vc.tabBarController as? BaseTBController {
let v = Int(tbc.valueData) == 0 ? Int(UserDefaults.standard.double(forKey: UDKeys.valueData)) : Int(tbc.valueData)
return "\(v)"
}
return ""
}
}
Then, from a view controller, you would call it like this:
let str = DataFieldOptions.showValueAs(vcRef: self)
Hi there,
Ive been coding an app for my friend and me recently and currently I'm implementing Google Firebase's Firestore Database. I have set up a Data Model and a View Model to handle data to my view. Bear in mind I'm still new to Swift(UI) so my code might be a little messy.
This is where the database is accessed and the data is put into the data model.
Friends_Model.swift
import Foundation
import Firebase
import FirebaseFirestore
class Friends_Model: ObservableObject {
#Published var friend_list = [Friends_Data]()
#Published var noFriends = false
func getData() {
let db = Firestore.firestore()
db.collection("users").getDocuments { snapshot, error in
//check for errors
if error == nil {
print("no errors")
if let snapshot = snapshot {
//Update the list property in main thread
DispatchQueue.main.async {
//get all docs and create friend list
self.friend_list = snapshot.documents.map { d in
//Create friend item for each document
return Friends_Data(id: d.documentID,
userID: d["userID"] as? String ?? "")
}
}
}
} else {
// handle error
}
}
}
}
This is my data model. To my understanding this just sets the variables.
Friends_Data.swift
import Foundation
struct Friends_Data: Identifiable {
var id: String
var userID: String
}
This is my actual view where I output the data (just the relevant part ofc).
FriendsPanel.swift (Swift View File)
// var body etc. etc.
if let user = user {
let uid = user.uid ?? "error: uid"
let email = user.email ?? "error: email"
let displayName = user.displayName
VStack {
Group{
Text("Your Friends")
.font(.title)
.fontWeight(.bold)
}
List (friends_model.friend_list) { item in
Text(item.userID)
}
.refreshable {
friends_model.getData()
}
}
// further code
Displaying all entries in the database works fine, though I'd wish to only display the entries with the attribute "friendsWith" having the same string as oneself (uid).
Something like
if friends_model.friends_list.userID == uid {
// display List
} else {
Text("You don't have any friends")
}
I couldn't work it out yet, although I've been going on and about for the past 2 hours now trying to solve this. Any help would be greatly appreciated. Also sorry if I forgot to add anything.
Load only the data you need:
Use a query:
let queryRef = db.collection("users").whereField("friendsWith", isEqualTo: uid)
and then:
queryRef.getDocuments { snapshot, error in......
Here you can find more about firestore:
https://firebase.google.com/docs/firestore/query-data/queries
You need to make a View that you init with friends_model.friend_list and store it in a let friendList. In that View you need an onChange(of: friendList) and then filter the list and set it on an #State var filteredFriendList. Then in the same view just do your List(filteredFriendList) { friend in
e.g.
struct FiltererdFriendView: View {
let friendList: [Friend] // body runs when this is different from prev init.
#State var filteredFriendList = [Friend]()
// this body will run whenever a new friendList is supplied to init, e.g. after getData was called by a parent View and the parent body runs.
var body: some View {
List(filteredFriendList) { friend in
...
}
.onChange(of: friendList) { fl in
// in your case this will be called every time the body is run but if you took another param to init that changed then body would run but this won't.
filteredFriendList = fl.filter ...
}
}
}
Goal of the code:
To assign a struct dictionary with Strings as Keys and String Arrays as values to a variable and then pull one (can be at random) specific String key value in the String Array and return that one String element in the underlying String Array so that it can be used elsewhere (potentially assigned to a label.text)
Essentially (please reference code below), I want to access one value at random in myDictionary using a specific key ("keyOne"), and pull, let's say, "Value2" then return only the string "Value2" from the underlying String Array associated with "keyOne" using indexing.
Errors are in the code below.
The issue I'm thinking is that I haven't figured out how to turn my final var Testing = dict["keyOne"] into an Int compatible index... if it was an index, the code would pull an Int value and the corresponding String from the three Strings in the underlying value array (due to the three String values associated with "keyOne").
Also, variableView() just inherits the datasource from several other containers, but the var dataSource : Structure? is the main reference, so that is what I included.
Code so far:
let myDictionary = [Structure(name: "keyOne", text: ["Value1", "Value2", "Value3"]), Structure(name: "keyTwo", text: ["Value4", "Value5", "Value6"])]
lazy var dict = Dictionary(uniqueKeysWithValues: myDictionary.lazy.map { ($0.name, $0.text) })
struct Structure: Hashable {
var name: String
var text: [String]
init(name: String, text: [String]){
self.name = name
self.text = text
}
}
func variable(at index: Int) -> variableView {
let variable = variableView()
var Testing = dict["keyOne"]
variable.dataSource = Testing![index] <- Cannot assign value of type 'String' to type 'structure'
return variable
var dataSource : Structure? {
didSet {
label.text = "This is a test"
} else {
// n/a
}
}
Please note that the error message is above in the code for variable.dataSource = Testing![index].
I am also suspecting that my issue lies in the "looping" logic of how I am assigning a variable with a struct, to a datasource which references that same struct.
Any help is appreciated as I have been stuck on this for legitimately a week (I truly have exhausted every single StackOverflow answer/question pair I could find).
THANK YOU!
EDIT:
I found this documentation to assist me greatly with this, and I recommend anyone with a similar question as mine to reference this: https://swift.org/blog/dictionary-and-set-improvements/
Given the question and the discussion in the comments I would add a mutating func to the struct that removes and returns a random string
mutating func pullText() -> String? {
guard let index = text.indices.randomElement() else {
return nil
}
return text.remove(at: index)
}
Example
if let index = myDictionary.firstIndex(where: { $0.name == "keyOne" }),
let text = myDictionary[index].pullText() {
someLabel.text = text
}
Here is another example based on the code in the question
Assuming VariableView looks something like this
struct VariableView: View {
var dataSource : Structure?
var word: String?
var body: some View {
Text(word ?? "")
}
}
Then the func variable can be changed to
func variable() -> VariableView {
var variable = VariableView()
if let index = dict.firstIndex(where: { $0.name == "keyOne" }) {
variable.dataSource = dict[index]
variable.word = dict[index].pullText()
}
return variable
}
My goal is to use a button (that contains multiple messages) to trigger a text (making a marker such as first click will be method 1, second click will be method 2) correspondingly added at the end of the my data (after joined(separator: "~")) so that it could help me to analyze which button was clicked when I look back at the data.
Currently, I have a struct that will output the data:
struct CaptureData {
var vertices: [SIMD3<Float>] //A vector of three scalar values. It will return a list of [SIMD3<Float>(x,y,z)]
var mode: Mode = .one
mutating func nextCase() { // the data method will be changed
mode = mode.next()
}
var verticesFormatted : String { //I formatted in such a way so that it can be read more clearly without SIMD3
let v = "<" + vertices.map{ "\($0.x):\($0.y):\($0.z)" }.joined(separator: "~") + "trial: \(mode.next().rawValue)"
return "\(v)"
}
}
Based on #Joshua suggestion
enum Mode: String, CaseIterable {
case one, two, three
}
extension CaseIterable where Self: Equatable {
var allCases: AllCases { Self.allCases }
var nextCase: Self {
let index = allCases.index(after: allCases.firstIndex(of: self)!)
guard index != allCases.endIndex else { return allCases.first! }
return allCases[index]
}
#discardableResult
func next() -> Self {
return self.nextCase
}
}
And the button is alternating the messages after each click,
var x = 0
var instance = CaptureData(vertices: [SIMD3<Float>])
// Next button for changing methods
#IBAction func ChangingTapped(_ btn: UIButton) {
if(x==0){
Textfield.text = "changing to driving"
}
else if(x==1){
Textfield.text = "changing to walking"
instance.nextCase()
}
else{
Textfield.text = "changing to cycling"
instance.nextCase()
}
x += 1
}
Updates: I am able to print one of the methods , .two (method two), after separator: "~". However, at the moment I am still not be able to click button to switch the case in the data.
The main problem is the initialization of variables. I am not able to define var instance = CaptureData(vertices: [SIMD3<Float>]) because it comes with error: Cannot convert value of type '[SIMD3<Float>].Type' to expected argument type '[SIMD3<Float>]'
I am sorry if my explanation is a bit messy here. I am trying to describe the problem I have here. Let me know if there is anything missing! Thank you so much in advance.
Enums is a data type that is more like a constant but much more readable.
An example will be passing in a status to a function.
enum Status {
case success
case failure
}
func updateStatus(_ status: Status) {
statusProperty = status
}
// somewhere in your code
instance.updateStatus(.success)
versus using an Int as a value.
func updateStatus(_ status: Int) {
statusProperty = status
}
// somewhere in your code
instance.updateStatus(1) // eventually you'll forget what this and you'll declare more of a global variable acting as constant, which technically what enums are for.
Enums in swift are a bit different though, much more powerful. More info about enums here
Back to the topic.
enum Mode: String, CaseIterable {
case one, two, three
}
extension CaseIterable where Self: Equatable {
var allCases: AllCases { Self.allCases }
var nextCase: Self {
let index = allCases.index(after: allCases.firstIndex(of: self)!)
guard index != allCases.endIndex else { return allCases.first! }
return allCases[index]
}
#discardableResult
func next() -> Self { // you don't need to update self here, remember self here is one of the items in the enum, i.e. one, so assigning one = two just doesn't work.
return self.nextCase
}
}
// The data struct
struct CaptureData {
var mode: Mode = .one
// we add a mutation function here so we can update the mode
mutating func nextCase() { // the data your concern about, that can actually mutate is the mode property inside CaptureData struct.
mode = mode.next()
}
}
So lets say somewhere in the app you can use it like this you initialised an instance of CaptureData:
var instance = CaptureData() // Don't forget it should be var and not let, as we are updating its property.
instance.nextCase() // get the next case, initially it was .one
print(instance.mode) // prints two
instance.nextCase() // from .two, changes to .three
print(instance.mode) // prints three
Hope this helps.
I am trying to load a value that has been inputted by the user in the viewDidLoad via a String. I am using UserDefaults to save the users value that they input into a UITextField (userValue), I then save this to the String 'search'. I am able to print out the value of search in the GoButton function, and it works fine, but when I load my ViewController as new, the value of 'search' is equal to nil. The aim here is to have the users previous search saved, and loaded into the UITextField (that is used as a search box) upon loading the ViewController.
Code Below:
class ViewController: UIViewController {
#IBOutlet weak var userValue: UITextField!
var search: String!
}
viewDidLoad:
override func viewDidLoad() {
if (search != nil)
{
userValue.text! = String (search)
}
}
Button Function:
#IBAction func GoButton(_ sender: Any) {
let userSearch: String = userValue.text!
let perference = UserDefaults.standard
perference.set(userSearch, forKey: "hello")
perference.value(forKey: "hello")
let value = perference.value(forKey: "hello") as! String
search = value
print (search) // <<this works, it prints out the users search value
}
#VishalSharma has the right idea, but the code should probably look more like…
override func viewDidLoad() {
super.viewDidLoad()
if let search = UserDefaults.standard.string(forKey: "hello") {
userValue.text = search
}
}
or even more simply…
userValue.text = UserDefaults.standard.string(forKey: "hello")
When you load, search is effectively nil.
So either you read userDefaults in viewDidload or you come through a segue: then you can load search in the prepare.
I've always found it convenient and useful to store all UserDefault properties as an extension within the same file along with their getters and setters. It is far easier to maintain, use and read. by using the #function keyword for the key you are referencing the variable's name and not a string that can be accidentally changed somewhere else in code.
UserDefaults.swift
import Foundation
// An Extension to consolidate and manage user defaults.
extension UserDefaults {
/// A value Indicating if the user has finished account setup.
/// - Returns: Bool
var finishedAcountSetup: Bool {
get { return bool(forKey: #function) }
set { set(newValue, forKey: #function) }
}
/// The hello text at the start of the application.
/// - Returns: String?
var helloText: String? {
get { return string(forKey: #function) }
set {set(newValue, forKey: #function) }
}
//etc...
}
When you use these values reference the standard settings:
//Setting
UserDefaults.standard.helloText = "Updated Hello Text"
// Getting
// for non-optional value you can just get:
let didCompleteSetup = UserDefaults.standard.finishedAcountSetup
// Otherwise, safely unwrap the value with `if-let-else` so you can set a default value.
if let text = UserDefaults.standard.helloText {
// Ensure there is text to set, otherwise use the default
label.text = text
} else {
// helloText is nil, set the default
label.text = "Some Default Value"
}
obviously, it provides nil because when view controller load the search is nil try this.
let perference = UserDefaults.standard
override func viewDidLoad() {
super.viewDidLoad()
if (perference.value(forKey: "hello") != nil) {
search = perference.value(forKey: "hello") as! String
userValue.text! = String (search)
}
}