Using Arrays in Singleton Class in iOS - ios

PetInfo.class
class PetInfo {
static let shared: PetInfo = PetInfo()
lazy var petArray: [PetInfo] = []
var PetID:Int
var PetName:String
...
init(){ .. }
}
ViewController.swift
class ViewController: UIViewController {
var PetArray = PetInfo.shared.petArray
override func viewDidLoad() {
super.viewDidLoad()
let pet = PetInfo()
pet.PetName = "Jack"
PetArray.append(pet). **Create Object and gave a name**
print(PetArray[0].PetName) //works!
}
}
secondViewController.swift
class secondViewController: UIViewController {
var PetArray = PetInfo.shared.petArray
override func viewDidLoad() {
super.viewDidLoad()
let label: UILabel = {
let label = UILabel()
...
label.text = PetArray[0].PetName **tried to print**
return label
}()
view.addSubview(label)
}
}
I want to share PetArray array in all of the view controllers.(It's more than two.)
It put data in the first VC, but doesn't work in the Second VC.
How can I share this array using a Singleton pattern?
Except for the array, It works perfect.(like String.. PetID, PetName.. )

Array in swift is implemented as Struct, which means Array is a value type and not a reference type. Value types in Swift uses copy on write (COW) mechanism to handle the changes to their values.
So in your ViewController when you assigned
var PetArray = PetInfo.shared.petArray
your PetArray was still pointing to the same array in your PetInfo.shared instance (I mean same copy of array in memory) . But as soon as you modified the value of PetArray by using
PetArray.append(pet)
COW kicks in and it creates a new copy of petArray in memory and now your PetArray variable in your ViewController and PetInfo.shared.petArray are no longer pointing to same instances instead they are pointing to two different copies of array in memory.
So all the changes you did by using PetArray.append(pet) is obviously not reflected when you access PetInfo.shared.petArray in secondViewController
What can I do?
remove PetArray.append(pet) and instead use PetInfo.shared.petArray.append(pet)
What are the other issues in my code?
Issue 1:
Never use Pascal casing for variable name var PetArray = PetInfo.shared.petArray instead use camel casing var petArray = PetInfo.shared.petArray
Issue 2:
class PetInfo {
static let shared: PetInfo = PetInfo()
lazy var petArray: [PetInfo] = []
var PetID:Int
var PetName:String
...
init(){ .. }
}
This implementation will not ensure that there exists only one instance of PetInfo exists in memory (I mean it cant ensure pure singleton pattern), though you provide access to instance of PetInfo using a static variable named shared there is nothing which stops user from creating multiple instances of PetInfo simply by calling PetInfo() as you did in let pet = PetInfo()
rather use private init(){ .. } to prevent others from further creating instances of PetInfo
Issue 3:
You are holding an array of PetInfo inside an instance of PetInfo which is kind of messed up pattern, am not really sure as to what are you trying to accomplish here, if this is really what you wanna do, then probably you can ignore point two (creating private init) for now :)

I think the best solution to use Combine and Resolver frameworks. Works perfectly in my case with shared arrays.
In your case it could be
import Combine
import Resolver // need to add pod 'Resolver' to Podfile and install it first
// Data Model
struct PetInfo: Codable {
var PetID:Int
var PetName:String
...
}
// Repository to read manage data (read/write/search)
class PetRepository: ObservableObject {
#Published var petArray = Array<PetInfo>()
override init() {
super.init()
load()
}
private func load() {
// load pets info from server
}
}
Need to add AppDelegate+Injection.swift that will contain repository registration:
import Foundation
import Resolver
extension Resolver: ResolverRegistering {
public static func registerAllServices() {
// Services
register { PetRepository() }.scope(application)
}
}
Then use it in any controllers
import UIKit
import Combine
import Resolver
class ViewController: UIViewController {
#LazyInjected var petRepository: PetRepository
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
petRepository.$petArray
.receive(on: DispatchQueue.main)
.debounce(for: 0.8, scheduler: RunLoop.main)
.sink { [weak self] petInfos in
// set UI here
}
.store(in: &cancellables)
}
}

If you want PetInfo to be a singleton, make its initializer private:
class PetInfo {
static let shared: PetInfo = PetInfo()
lazy var petArray: [PetInfo] = []
var PetID:Int
var PetName:String
...
private init(){ .. } // !!
}
This way, any attempt to create new instances (like you do in your first ViewController) will fail, and will remind you to always use PetInfo.shared to access the singleton.

Related

How to reload UITableView from different controller

I am attempting to make a to-do app with a controller for both "this week" and "next week." (Objects with a date in the current week will be shown in the thisWeek view controller, while objects with a date next week will be shown in the nextWeek view controller. However, sometimes objects will not appear in the table until the app restarts, and I'm not sure why this is happening or how to fix it.
My theory is that each table in the different controllers needs to be reloaded each time data is added/changed. I just don't know how to access the table from a different controller than the one I am in.
Attempt 1 I tried to use a singleton that looked like this:
class ToDoTables {
private init() {}
static let shared = ToDoTables()
lazy var thisTable: UITableView = {
let thisWeek = UITableView()
return thisWeek
}()
lazy var nextTable: UITableView = {
let nextWeek = UITableView()
return nextWeek
}()
}
then to be called like:
let thisTable = ToDoTables.shared.thisTable
However this did not work because I initially created the table using storyboard and just used an IBOutlet in the controller, and couldn't find way to keep the outlet and also use singleton. Next, I tried to remove the outlet and just use the code above. When I simulated the app, it crashed because the outlet couldn't be found or something like that (which was expected).
Attempt 2
I tried to access the other controller's table by creating an instance of the vc and accessing the table that way:
NextWeekController().nextTable.reloadData()
but got an error that said "Unexpectedly found nil while implicitly unwrapping an Optional value"
So I'm not sure if that is my tables need to be reloaded each time, or if it is something else.
Overview of non-functioning operations (an app restart is needed, or the controller/table needs loaded again):
something created in the nextWeek controller with a date in the current week will not appear
when an object in created in nextWeek controller, then date is changed from next week to this week
something created in the thisWeek controller with a date in the next week will not appear
when an object in created in thisWeek controller, then date is changed from this week to next week
My theory is that each table in the different controllers needs to be reloaded each time data is added/changed.
You are correct. For UITableView.reloadData(), the Apple Docs state to:
Call this method to reload all the data that is used to construct the
table
In other words, it is necessary to reload the table view's data with this method if the data is changed.
I just don't know how to access the table from a different controller
than the one I am in.
One solution is to create global references to all your view controllers through one shared instance.
// GlobalVCs.swift
class GlobalVCs {
static var shared = GlobalVCs()
var nameOfVC1!
var nameOfVC2!
var nameOfVC3!
// and so on
private init() {}
}
Then set the global vc references from each view controller's viewDidLoad() method.
Example:
// VC1.swift
override func viewDidLoad() {
GlobalVCs.shared.vc1 = self
}
Then you can access a view controller anywhere with GlobalVCs.shared.vc1, GlobalVCs.shared.vc2, etc.
Sorry, but need to say that this is wrong approach. We need to follow to MCV model where we have data model, view and controller. If you example for to-do model you could have:
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
struct ToDoItem: Codable, Identifiable {
static let collection = "todos"
#DocumentID var id: String?
var uid: String;
var done: Bool;
var timestamp: Timestamp;
var description: String
}
Then you need to have repository for this type of data, for example if we are using Firebase we can have
import Foundation
import Combine
import FirebaseFirestore
import FirebaseFirestoreSwift
class BaseRepository<T> {
#Published var items = Array<T>()
func add(_ item: T) { fatalError("This method must be overridden") }
func remove(_ item: T) { fatalError("This method must be overridden") }
func update(_ item: T) { fatalError("This method must be overridden") }
}
and then we have ToDoRepository (singleton object)
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
import Resolver
import CoreLocation
import Combine
class ToDoRepository: BaseRepository<ToDoItem>, ObservableObject {
var db = Firestore.firestore()
uid: String? {
didSet {
load()
}
}
private func load() {
db.collection(ToDoItem.collection).whereField("uid", isEqualTo: uid).order(by: "timestamp").addSnapshotListener {
( querySnapshot, error ) in
if let querySnapshot = querySnapshot {
self.items = querySnapshot.documents.compactMap { document -> ToDoItem? in
try? document.data(as: ToDoItem.self)
}
}
}
}
}
Now we need to register repository in AppDelegate+Injection.swift:
import Foundation
import Resolver
extension Resolver: ResolverRegistering {
public static func registerAllServices() {
// Services
register { ToDoRepository() }.scope(application)
}
}
After that we can use ToDoRepository in any controller :
import UIKit
import Combine
import Resolver
class MyController: UIViewController {
private var cancellables = Set<AnyCancellable>()
#LazyInjected var toDoRepository: ToDoRepository
// and listen and update any table view like this
override func viewDidLoad() {
super.viewDidLoad()
toDoRepository.$items
.receive(on: DispatchQueue.main)
.sink { [weak self] todos
self?.todos = todos
tableView.reloadData()
}
.store(in: &cancellables)
}
}

Storing data in-between segues

I have a tableView, it is populated with car names. User can add, delete cells.
CarData.swift:
struct CarData {
var name: String
}
Cars.swift:
class Cars{
var list = [
CarData(name: "Toyota Corolla"),
CarData(name: "BMW 3")
]
}
ListViewController.swift:
class ListViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var carsList = Cars()
var cars = [CarData]()
override func viewDidLoad() {
super.viewDidLoad()
cars = carsList.list
.
.
.
When a segue brings user to the ListViewController, the list is always going to be the same due to the way cars array is initialized. What I want is the list to save its data in-between segues and not to be rewritten every time. I have a lot of segues, so sending cars array all around doesn't seem to be a great idea. What should I do?
Global Access!
For your begging level, you can create a singleton object to access it globally across the entire application.
class CarsManager
{
// MARK: Init
private init() { }
// MARK: Properties
static var shared = CarsManager()
private var cars: [CarData] = []
// MARK: Methods
func addNewCar(_ car: CarData)
{
cars.append(car)
}
func getCars() -> [CarData]
{
return cars
}
}
Usage
You can add cars to your list on any screen:
let someCar = CarData(name: "Toyota Corolla")
CarsManager.shared.addNewCar(someCar)
Then you can easily access your list from all screens:
CarsManager.shared.getCars()
What to do next?
You should read more about the pros and cons of the singleton design pattern to know when to use and when not:
What Is a Singleton and How To Create One In Swift
Singleton in Swift
Swift Singletons: A Design Pattern to Avoid (With Examples)

Use function from a structure in class

I am new to Swift and I have trouble using classes and structures.
I have a Structure called Workspace:
struct Workspace: Decodable {
var guid: String
var name: String
func getUserWorkspace(base: String, completed: #escaping () -> ()){
//some code
}
}
Here is my class User:
public class User {
var Wor = [Workspace]()
var WorData:Workspace? = nil
//+some other var & functions
}
So what I'm doing in my view controller is this:
class SecondViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var listView: UITableView!
var co = User()
override func viewDidLoad() {
super.viewDidLoad()
co.WorData?.getUserWorkspace(base: co.Base) {
print("success")
self.listView.reloadData()
self.updateVertically()
}
listView.delegate = self
listView.dataSource = self
}
The problem is that the code never goes inside the function co.WorData?.getUserWorkspace(base: co.Base)
Before I put it in the structure it was directly in the class but since I changed it it doesn't work anymore so I think I might be calling it the wrong way ?
WorData is nil.
Conditional unwrapping (co.WorData?.getUserWorkspace(base: co.Base) will check WorData has a value before trying to call the method. If it was nil and Swift didn't do this, it would crash.
You either need to set it as new all the time
var worData = Workspace()
Set it after class init
var user = User()
user.worData = Workspace() // or pass a specific one in
or require your User object to be initialised with a Workspace
class User: NSObject {
var wor = [Workspace]()
var workspace: Workspace // use lower camel case for var names
required init(workspace: Workspace) {
self.workspace = workspace
}
}

#obj protocol delegate method not working in second class. Swift

Im trying to call protocol delegate in an additional class. The first class (ViewController) works but the second one I have implemented it in doesn't show any data. I added it with the autofill option so its not giving errors. It just doesn't do anything.
sender class
#objc protocol BWWalkthroughViewControllerDelegate{
#objc optional func walkthroughPageDidChange(pageNumber:Int) // Called when current page changes
}
#objc class BWWalkthroughViewController: UIViewController, UIScrollViewDelegate, ViewControllerDelegate {
// MARK: - Public properties -
weak var delegate:BWWalkthroughViewControllerDelegate?
var currentPage:Int{ // The index of the current page (readonly)
get{
let page = Int((scrollview.contentOffset.x / view.bounds.size.width))
return page
}
}
// MARK: - Private properties -
let scrollview:UIScrollView!
var controllers:[UIViewController]!
var lastViewConstraint:NSArray?
var shouldCancelTimer = false
var aSound: AVAudioPlayer!
var isForSound: AVAudioPlayer!
var alligatorSound: AVAudioPlayer!
var audioPlayer = AVAudioPlayer?()
var error : NSError?
var soundTrack2 = AVAudioPlayer?()
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
var audioPlayerAnimalSound = AVAudioPlayer?()
var audioPlayerAlphabetSound = AVAudioPlayer?()
var audioPlayerFullPhraze = AVAudioPlayer?()
var audioPlayerPhonics = AVAudioPlayer?()
Code removed to save space carries on:
/**
Update the UI to reflect the current walkthrough situation
**/
private func updateUI(){
// Get the current page
pageControl?.currentPage = currentPage
// Notify delegate about the new page
delegate?.walkthroughPageDidChange?(currentPage)
}
receiver class
class BWWalkthroughPageViewController: UIViewController, BWWalkthroughPage, ViewControllerDelegate, BWWalkthroughViewControllerDelegate {
Function in second Class.
func walkthroughPageDidChange(pageNumber: Int) {
println("current page 2 \(pageNumber)")
}
walkthroughPageDidChange does work in the viewController class however. Please can you help me see what is wrong?
Your weak var delegate:BWWalkthroughViewControllerDelegate? is an optional variable and it is not assigned anywhere in your presented code, hence it will be nil. You have to instantiate it somewhere for it to work.
A good way to do so is:
class BWWalkthroughViewController {
let bwWalkthroughPageViewController = BWWalkthroughPageViewController()
var bwWalkthroughViewControllerDelegate : BWWalkthroughViewControllerDelegate!
init() {
bwWalkthroughViewControllerDelegate = bwWalkthroughPageViewController as BWWalkthroughViewControllerDelegate
}
}
A thing to note is that it is often good practice to name the delegates with meaningful names. The keyword "delegate" is often used for many classes and is restricted. A more verbose name will eliminate the chance of overriding an original delegate and will help identify each one if you have more than one.

What is best practice for global variables and functions in Swift?

I coding an app with several (15-25 different swigft files one for each view.
Some variables and functions I will use in every viewcontroller.
What would be best practice to enable code reusage?
For instance I need to communicate with a server in which the first request is for an access token, this request I imagine could be a global function setting a global variable (access token). And then using it for the more specific requests.
I started placing a lot of global constants in appdelegate file, in a Struct is there a problem with this?
LibraryAPI.swift
import UIKit
import CoreData
class LibraryAPI: NSObject
{
let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext
private var loginD: LoginDetails
private var isOnline: Bool
class var sharedInstance: LibraryAPI
{
struct Singleton
{
static let instance = LibraryAPI()
}
return Singleton.instance
}
override init()
{
super.init()
}
func getIsOnline() -> Bool
{
return isOnline
}
func setIsOnline(onlineStatus: Bool)
{
isOnline = onlineStatus
}
func getLoginDetails() -> LoginDetails
{
return loginD
}
func setLoginDetails(logindet: LoginDetails)
{
loginD = logindet
}
// Execute the fetch request, and cast the results to an array of objects
if let fetchResults = managedObjectContext!.executeFetchRequest(fetchRequest, error: nil) as? [LoginDetails] {
setLoginDetails(fetchResults[0])
}
}
You should avoid using global variables.
Depending on what you have / what you need to do, either you can :
Create a class and make an instance on your first call. Then, you can pass the object through your views (prepareForSegue). But that can still be repetitive to implement everytime ;
Create a singleton class that will be instantiate only once and accessible from everywhere (singleton are considered as a bad practice by some);
Use the NSUserDefaults to store String ;
Save your data somehow (CoreData, ...).
You can do like this
User.swift
import Foundation
import UIKit
class User: NSObject {
var name: String = ""
func getName() -> String{
name = "Nurdin"
return name
}
}
ViewController.swift
import Foundation
import UIKit
let instanceOfUser = User()
println(instanceOfUser.getName()) // return Nurdin

Resources