Pass array from AppDelegate to view controller - arrays

I'm loading an array with data from a Firebase database in my AppDelegate as I need the arrays loaded before the views are created and loaded. How can I pass this array to a view controller to populate a TableView?
Edit:
I don't think I worded my question correctly nor provided enough information. I have this Firebase data that I need to load into an array. This array needs to be loaded into the app before it is used by the view controller that uses the array. This is because the view controller uses a cocoa pod that splits the array into a number of categories to be displayed in different tableviews. The repo for the cocoa pod I'm using can be found here.
My question then is where is the best place to load this array? My first thought was the AppDelegate, but the array is empty and as such the table view doesn't load. I'm pretty new to iOS programming so I'm open to any and all suggestions.

In the AppDelegate class:
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
//Fill your array, let's say it's called firebaseArray. I would recommend doing so in the view controller and not in the Appdelegate for better responsibility distribution in your code
let vc = YourViewController()
//And YourViewController must have a array property
vc.array = firebaseArray
window = UIWindow(frame: UIScreen.main.bounds)
//make your vc the root view view controller
window?.rootViewController = vc
window?.makeKeyAndVisible()
return true
}

Related

What is the best and quickest way to save a large Class Array in existing project? Realm isn't working

First off, I'm a relative amateur with app design having taught myself high-level swift/xcode last year, so apologies for my code in advance!
I've developed a game which has an array of a 'Player' Class called playerList. I currently convert this playerList array to JSON via Encoder and then save to device...however as my array grows, this exercise is beginning to take a long time, so I'm looking for an alternative. I presume the best solution is to rewrite the app to use CoreDate, SQLite etc, but I'm looking for a quick solution for now.
I could have used userDefaults, however steered away from this as large array and am instead trying to fudge a solution using Realm.
I've attempted the below, but whenever I look at my playerList after loading it is empty. Am I missing something obvious here, or alternatively is there a much better approach than using Realm?
class PlayerArray: Object {
var iden: Int = 0
var allThePlayers: [Player] = playerList
}
func saveViaRealm() {
// Get the default Realm
let realm = try! Realm()
// Define player list
let realmPlayerList = PlayerArray()
realmPlayerList.allThePlayers = playerList
realmPlayerList.iden = 1
// Write to realm
try! realm.write {
realm.add(realmPlayerList)
}
}
func loadViaRealm() {
// Get the default Realm
let realm = try! Realm()
//Retrieve objects from realm
let realmOutputPlayerList = realm.objects(PlayerArray.self)
// Filter to iden required
let realmFiltered = realmOutputPlayerList.filter{$0.iden == 1}[0]
// Assign to playerList
playerList = realmFiltered.allThePlayers
}
I would take a read through the Realm documentation once more around Lists and declaring variables. In your object class are you getting any errors? RealmSwift should be declared with #objc dynamic vars. Also, you shouldn't need but one let = realm. Here is the link to Realm.io documentation.

Save and load object data with NSKeyedArchiver - data loss after object variables changed

I am using NSKeyedArchiver() for saving and loading objects in Swift.
The problem is when the object variables change, like adding new variable into object, the NSKeyedArchiver() cannot decode the last saved objects.
func tripsDataFilePath() -> URL {
return getDocumentsDirectory().appendingPathComponent("Data.plist")
}
The code I am using for saving:
func saveData() {
NSKeyedArchiver.archiveRootObject(data, toFile: dataFilePath().path)
}
and the loading code:
func loadData() {
if let data = NSKeyedUnarchiver.unarchiveObject(withFile: dataFilePath().path) {
project = data as! [Project]
}
}
Is there any way to prevent data loss (can't load last objects) for when changing the object structure?
It is not important when developing first version of iOS app. But imagine that users downloaded the application and in new versions I want to add new features that requires changes to current data objects! then users will loss all datas.
i have find out that the problem is with file format.
i have used this name for saving NSKeyedArchiver Object : "Data.plist"
and when i have removed ".plist" format, the problem has been solved.
Try to check how you decode your data. I was decoding all the data with decodeObject. However, from swift 3.0 you need to be specific how to decode your data. For example, if your data is an Integer, instead of using
let number = aDecoder.decodeObject(forKey: CodingKeys.number) as? Int
you need to use
let number = aDecoder.decodeInteger(forKey: CodingKeys.number)

How can I assert that a value of a string is equal to one of any string values in an array using XCTAssert in Swift?

I'm new to Swift and followed a simple tutorial to make a magic 8 ball Cocoa App that every time I click the ball it shows a different piece of advice. I am now trying to practice my UI automated tests by asserting (XCTAssert) that the "Piece of Advice" label is equal to one of the string values in my array.
My array looks like this and is in my ViewController.swift:
var adviceList = [
"Yes",
"No",
"Tom says 'do it!'",
"Maybe",
"Try again later",
"How can I know?",
"Totally",
"Never",
]
How can I make an assertion in my UITests.swift file that asserts that the string that is shown is equal to one of the string values in the array above?
It's possible that you're asking how to access application state from a UI test, or just in general UI testing.
I think it's a pretty interesting question so I'm going to answer because it's something that I don't know a lot about and hopefully will prompt other people to chime in and correct.
Background: A basic Magic 8 Ball project
I set up a basic project with a view controller that contains two views: a label and a button. Tapping the button updates the label text with a random message:
import UIKit
struct EightBall {
static let messages = ["Yes", "No", "It's not certain"]
var newMessage: String {
let randomIndex = Int(arc4random_uniform(UInt32(EightBall.messages.count)))
return EightBall.messages[randomIndex]
}
}
class ViewController: UIViewController {
let ball = EightBall()
#IBOutlet weak var messageLabel: UILabel!
#IBAction func shakeBall(_ sender: Any) {
messageLabel.text = ball.newMessage
}
}
A basic UI test
Here's a commented UI test showing how to automate tapping on the button, and grabbing the value of the label, and then checking that the value of the label is a valid message.
import XCTest
class MagicUITests: XCTestCase {
// This method is called before the invocation of each test method in the class.
override func setUp() {
super.setUp()
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = true
// UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
XCUIApplication().launch()
}
func testValidMessage() {
// Grab reference to the application
let app = XCUIApplication()
// #1
// Grab reference to the label with the accesability identifier 'MessageLabel'
let messagelabelStaticText = app.staticTexts["MessageLabel"]
// Tap the button with the text 'Shake'
app.buttons["Shake"].tap()
// get the text of the label
let messageLabelText = messagelabelStaticText.label
// #2
// check if the text in the label matches one of the allowed messages
let isValidMessage = EightBall.messages.contains(messageLabelText)
// test will fail if the message is not valid
XCTAssert(isValidMessage)
}
}
At #1 The approach that I'm using to get the label is to access the labels accessibilityIdentifier property. For this project I entered this through storyboard, but if you're setting your views up in code you can directly set the accessibilityIdentifier property yourself.
The other thing that's confusing here is that to get access to elements in the view you're not navigating the view hierarchy, but a proxy of the hierarchy, which is why the syntax to get a label is the odd 'staticTexts' (The references at the bottom of the post explain this in more detail).
For #2 I'm inspecting the structure defined in my project. In a unit test you could access this my importing #testable import ProjectName but unfortunately this approach doesn't work for UI Test.
Instead, you'll have to make sure that any source file you want to access from the UI test is included as a target. You can do this in Xcode from this panel by checking the name of your UI test:
More UI testing references:
UI Testing Intro: http://www.mokacoding.com/blog/xcode-7-ui-testing/
UI Testing Cheat Sheet: http://masilotti.com/ui-testing-cheat-sheet/

Array of entity does not mutate in swift?

User Entity Model-
class UserEntity: NSObject {
var isAlreadyUser:Bool
init(isAlerdy:Bool){
isAlreadyUser = isAlerdy
}
}
App Delegate / Global Array
let new = ["F","E","D","C","B","A"]
for obj in new{
arrUser.append(UserEntity(isAlerdy: false))
}
VIEW CONTROLLER
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let home = Array(appDelegate.arrUser)
home[0].isAlreadyUser = true
print(appDelegate.arrUser[0].isAlreadyUser)
After I edit the local home array and update isAlreadyUser to true from false. This also changes in global array. Even I am mutating any making a copy of global array it still changes it i both the array.
I think some thing is wrong with entity. It is strong and not changing according to local scope of array.
Help me out.
EDIT:
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
var areAlreadyUsers:[UserEntity] = []
areAlreadyUsers = Array(appDelegate.arrUser)
areAlreadyUsers[0].isAlreadyUser = true
print(appDelegate.arrUser[0].isAlreadyUser)
Still no help. My global array is still changing.
If you were to make the UserEntity into a struct, you would not have the problem. Does it need to be an NSObject if not try using struct instead of class. This way you get copy on write behavior.
Problem:
The elements of the array are referenceType hence only the pointers to the objects will be copied. Hence modifying them will reflect in both the places.
Solution:
Copy each item contained in the array.
you can do some thing like, let localArray = globalArray.map {
$0.copy() }. Please note that it is important your object should
implement any of the copy methods such as copyWithZone.
Use a value type such as struct instead of class.
The point is that arrUser and home just contain pointers to the same UserEntity objects, which are unique (not copied).
So what you do you just change a value of a UserEntity property and it will obviously be reflected everywhere.
What you want to do is to rethink your app architecture, why would you mutate this value at all? For instance, you can create an array
var areAlreadyUsers:[UserEntity] = []
and just save there pointers to users that you would give true value. This way this information isn't saved anywhere.

Reload UICollectionView header or footer?

I have some data that is fetched in another thread that updates a UICollectionView's header. However, I've not found an efficient way of reloading a supplementary view such as a header or footer.
I can call collectionView reloadSections:, but this reloads the entire section which is unnecessary. collectionView reloadItemsAtIndexPaths: only seems to target cells (not supplementary views). And calling setNeedsDisplay on the header itself doesn't appear to work either. Am I missing something?
You can also use (the lazy way)
collectionView.collectionViewLayout.invalidateLayout() // swift
[[_collectionView collectionViewLayout] invalidateLayout] // objc
More complex would be to provide a context
collectionView.collectionViewLayout.invalidateLayout(with: context) // swift
[[_collectionView collectionViewLayout] invalidateLayoutWithContext:context] // objc
You can then make a or configure the context yourself to inform about what should be updated see: UICollectionViewLayoutInvalidationContext
It has a function in there that you can override:
invalidateSupplementaryElements(ofKind:at:) // swift
Another option is (if you have already loaded the correct header/footer/supplementary view) and you only want to update the view with the new data than you can use one of the following functions to retrieve it:
supplementaryView(forElementKind:at:) // get specific one
visibleSupplementaryViews(ofKind:) // all visible ones
Same goes for visible cells with visibleCells. The advantage of just getting the view and not reloading a view entirely is that the cells retains it state. This is espically nice with table view cells when they use swipe to delete/edit/etc since that state is lost after reloading the cell.
If you feel fanatic you can of course also write some extensions to retrieve only cells/supplementary views of a given kind using generics
if let view = supplementaryView(forType: MySupplementaryView.self, at: indexPath) {
configure(view, at indexPath)
}
this assumes that you have a function that registers/dequeues views in example with their class name. I made a post about this here
I just ran into the same problem, and I ended up looking up the view using its tag to edit a label:
UICollectionReusableView *footer = (UICollectionReusableView*)[self.collectionView viewWithTag:999];
UILabel *footerLabel = (UILabel*)[footer viewWithTag:100];
Like you said it is unnecessary to reload an entire section, which cancels out any animation on cells as well. My solution isn't ideal, but it's easy enough.
Swift 3/4/5 version:
collectionView.collectionViewLayout.invalidateLayout()
Caution!
If you change the number of collectionView items at the same time (for example you show the footer only if all cells were loaded), it will crash. You need to reload the data first, to make sure that the number of items is the same before and after invalidateLayout():
collectionView.reloadData()
collectionView.collectionViewLayout.invalidateLayout()
I got the same problem. I tried #BobVorks's answer and it is working fine, if only the cell was reused else it won't. So, I tried finding a more cleaner way to achieve this and I came up reloading the whole UICollectionView after the performBatchUpdate (completion block) and it is working great. It reloads the Collection Without any cancellation of animation in the insertItemsAtIndexPath. Actually I personally up voted recent 2 answers cause i find it working but in my case, this is the cleanest way to do it.
[self.collectionView performBatchUpdates:^{
// perform indexpaths insertion
} completion:^(BOOL finished) {
[self.collectionView reloadData];
}];
[UIView performWithoutAnimation:^{
[self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:4]];
}];
[UIView performWithoutAnimation:^{
[self.collectionView reloadData];
}];
Here are two ways you could do it.
1.
Create a mutable model to back the data that will eventually be available. Use KVO in inherited class of UICollectionReusableView to observe the changes and update the header view with the new data as it comes available.
[model addObserver:headerView
forKeyPath:#"path_To_Header_Data_I_care_about"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:NULL];
then implement listener method in header view
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
2.
add notification listener to the view and post a notification when the data has successfully come available. Downside is that this is application wide and not a clean design.
// place in shared header file
#define HEADER_DATA_AVAILABLE #"Header Data Available Notification Name"
// object can contain userData property which could hole data needed.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(headerDataAvailable:) name:HEADER_DATA_AVAILABLE object:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:HEADER_DATA_AVAILABLE object:nil];
let headerView = collectionView.visibleSupplementaryViews(ofKind: UICollectionView.elementKindSectionHeader)[0] as! UICollectionReusableView
I've used above method to get current header, and successfully updated subviews on it.
Here's what I did to update only the section headers that are currently loaded in memory:
Add a weakToStrong NSMapTable. When you create a header, add the header as the weakly held key, with the indexPath object. If we reuse the header we'll update the indexPath.
When you need to update the headers, you can now enumerate the objects/keys from the NSMapTable as needed.
#interface YourCVController ()
#property (nonatomic, strong) NSMapTable *sectionHeaders;
#end
#implementation YourCVContoller
- (void)viewDidLoad {
[super viewDidLoad];
// This will weakly hold on to the KEYS and strongly hold on to the OBJECTS
// keys == HeaderView, object == indexPath
self.sectionHeaders = [NSMapTable weakToStrongObjectsMapTable];
}
// Creating a Header. Shove it into our map so we can update on the fly
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
PresentationSectionHeader *header = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:#"presentationHeader" forIndexPath:indexPath];
// Shove data into header here
...
// Use header as our weak key. If it goes away we don't care about it
// Set indexPath as object so we can easily find our indexPath if we need it
[self.sectionHeaders setObject:indexPath forKey:header];
return header;
}
// Update Received, need to update our headers
- (void) updateHeaders {
NSEnumerator *enumerator = self.sectionHeaders.keyEnumerator;
PresentationSectionHeader *header = nil;
while ((header = enumerator.nextObject)) {
// Update the header as needed here
NSIndexPath *indexPath = [self.sectionHeaders objectForKey:header];
}
}
#end
This question is very old but a simple way to do it is to just set a delay that covers the time your view is animating and disabling the animation while you update the view...usually a delete or insert takes about .35 seconds so just do:
delay(0.35){
UIView.performWithoutAnimation{
self.collectionView.reloadSections(NSIndexSet(index: 1))
}
My problem arose when frame sizes for the supplementary views changed upon invalidating the layout. It appeared that the supplementary views were not refreshing. It turns out they were, but I was building the UICollectionReusableView objects programmatically, and I was not removing the old UILabel subviews. So when the collection view dequeued each header view, the UILabels would pile up, causing erratic appearance.
The solution was to build each UICollectionReusableView completely inside the viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath method, starting by a) removing all subviews from the dequeued cell, then b) getting the frame size from the item's layout attributes to allow adding the new subviews.
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
yourClass *header = (yourClass *)[collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:#"identifier" forIndexPath:indexPath];
[[header viewWithTag:1] removeFromSuperview]; // remove additional subviews as required
UICollectionViewLayoutAttributes *attributes = [collectionView layoutAttributesForSupplementaryElementOfKind:kind atIndexPath:indexPath];
CGRect frame = attributes.frame;
UILabel *label = [[UILabel alloc] initWithFrame: // CGRectMake based on header frame
label.tag = 1;
[header addSubview:label];
// configure label
return header;
}
I have got a Perfect solution:
let footerView = self.collectionView.visibleSupplementaryViews(ofKind: UICollectionView.elementKindSectionFooter)
Now you can access all subview of footerView by using:
footerView[0].subviews[0]
If you have label in your footerView then :
let label: UILabel = footerView[0].subviews[0] as? UILabel ?? UILabel()
Final Step:
label.text = "Successfully Updated Footer."
if let footerView = collectionView.subviews.first(where: {$0 is LoadingFooterCell}) as? LoadingFooterCell {
footerView.isLoading = .loading
}

Resources