My app's startup screen is a collection view. If the users choose, he/she can "lock" the app using Face ID/Touch ID at app launch. I do this by presenting a view controller that contains a UIVisualEffectView
on top of my collection view controller. I screenshot my collection view controller in viewDidLoad
, then put the screenshot in a UIImageView
beneath the UIVisualEffectView
.
The problem is, the collection view is not loaded when the screenshot is taken and the UIVisualEffectView
controller appears. The screenshot contains the navigation bar, but the content of the view is just black. I call the function to display the visual effect view controller in the collection view's viewDidLoad
function.
Is there a way to load the collection view's data before I take a screenshot in viewDidLoad
? The data is stored in Core Data. I have already tried moving the Core Data functions from viewWillAppear
to viewDidLoad
, but that also didn't work.
EDIT: I was able to solve my problem, but not by loading my UICollectionView
's data in ViewDidLoad, as that seems to be impossible. I added a UIVisualEffectView
directly on top of my UICollectionView
. I also added it as a subview to UIApplication.shared.keyWindow
so that it would appear above the navigation bar. The edited code is down below.
Here is my code:
For the collection view controller:
import UIKit
import CoreData
import LocalAuthentication
var ssImage: UIImage?
class AlbumViewController: UIViewController {
// MARK: - Properties
@IBOutlet weak var albumCollectionView: UICollectionView!
var albums: [NSManagedObject] = []
// MARK: - Actions
func getScreenShot()-> UIImage? {
var screenshotImage :UIImage?
let layer = UIApplication.shared.keyWindow!.layer
let scale = UIScreen.main.scale
UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, scale);
guard let context = UIGraphicsGetCurrentContext() else {return nil}
layer.render(in:context)
screenshotImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
ssImage = screenshotImage
print("Got screenshot.")
return screenshotImage
}
// ViewDidLoad and ViewWillAppear
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//Core Date functions
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext =
appDelegate.persistentContainer.viewContext
let fetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "Album")
let sortDescriptor = NSSortDescriptor(key: "albumName", ascending: true, selector: #selector(NSString.localizedCaseInsensitiveCompare(_:)))
fetchRequest.sortDescriptors = [sortDescriptor]
do {
albums = try managedContext.fetch(fetchRequest)
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
//Setup
self.albumCollectionView.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
if AppSettings.requiresLogin == true {
getScreenShot()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let rootController = storyboard.instantiateViewController(withIdentifier: "authenticateVC") as! LockedLaunchVC
self.present(rootController, animated: false, completion: nil)
}
}
//Core Data functions
func save(name: String) {
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext =
appDelegate.persistentContainer.viewContext
let entity =
NSEntityDescription.entity(forEntityName: "Album",
in: managedContext)!
let albumName = NSManagedObject(entity: entity,
insertInto: managedContext)
albumName.setValue(name, forKeyPath: "albumName")
do {
try managedContext.save()
albums.append(albumName)
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
}
}
//Collection View functions
extension AlbumViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return albums.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let reuseIdentifier = "AlbumCell"
// get a reference to our storyboard cell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath as IndexPath) as! AlbumsViewCell
//Core Data methods
let albumName = albums[indexPath.row]
cell.albumNameLabel?.text = albumName.value(forKeyPath: "albumName") as? String
//Return the finished cell
return cell
}
}
For the UIVisualEffectView controller:
import UIKit
import LocalAuthentication
class LockedLaunchVC: UIViewController {
let bioIDAuth = BiometricIDAuth()
@IBOutlet weak var screenshotImageView: UIImageView!
@IBAction func unlockButtonTapped(_ sender: UIButton) {
authAndDismiss()
}
func authAndDismiss(){
bioIDAuth.authenticateUser() {
self.dismiss(animated: true, completion: nil)
print("Locked VC dismissed.")
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.modalTransitionStyle = .crossDissolve
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
screenshotImageView.image = ssImage
}
}
EDIT: Revised code for my collection view controller:
import UIKit
import CoreData
class AlbumViewController: UIViewController {
// MARK: - Properties
@IBOutlet weak var albumCollectionView: UICollectionView!
@IBOutlet weak var lockedBlurView: UIVisualEffectView!
@IBOutlet weak var lockedLabelView: UIView!
@IBOutlet weak var lockedLabel: UILabel!
var albums: [NSManagedObject] = []
let bioIDAuth = BiometricIDAuth()
// MARK: - Actions
@IBAction func unwindToAlbumsScreen(sender: UIStoryboardSegue) {
}
@IBAction func unlockButtonTapped(_ sender: UIButton) {
bioIDAuth.authenticateUser() {
UIView.animate(withDuration: 0.25, animations: {self.lockedBlurView.alpha = 0})
}
}
// ViewDidLoad and ViewWillAppear
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//Core Date functions
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext =
appDelegate.persistentContainer.viewContext
let fetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "Album")
let sortDescriptor = NSSortDescriptor(key: "albumName", ascending: true, selector: #selector(NSString.localizedCaseInsensitiveCompare(_:)))
fetchRequest.sortDescriptors = [sortDescriptor]
do {
albums = try managedContext.fetch(fetchRequest)
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
//Setup to do when the view will appear
self.albumCollectionView.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
lockedLabelView.layer.cornerRadius = 12
switch bioIDAuth.biometricType() {
case .faceID:
lockedLabel.text = "Albums are locked. Tap anywhere to use Face ID to unlock."
case .touchID:
lockedLabel.text = "Albums are locked. Tap anywhere to use Touch ID to unlock."
default:
lockedLabel.text = "Albums are locked. Tap anywhere to use your passcode to unlock."
}
if AppSettings.requiresLogin == false {
lockedBlurView.alpha = 0
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if AppSettings.requiresLogin == true {
let curWin = UIApplication.shared.keyWindow
curWin?.addSubview(lockedBlurView)
}
}
//Core Data functions
func save(name: String) {
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext =
appDelegate.persistentContainer.viewContext
let entity =
NSEntityDescription.entity(forEntityName: "Album",
in: managedContext)!
let albumName = NSManagedObject(entity: entity,
insertInto: managedContext)
albumName.setValue(name, forKeyPath: "albumName")
do {
try managedContext.save()
albums.append(albumName)
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
}
}
//Collection View functions
extension AlbumViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return albums.count
}
// make a cell for each cell index path
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let reuseIdentifier = "AlbumCell"
// get a reference to our storyboard cell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath as IndexPath) as! AlbumsViewCell
//Core Data methods
let albumName = albums[indexPath.row]
cell.albumNameLabel?.text = albumName.value(forKeyPath: "albumName") as? String
//Return the finished cell
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let albumNameFromCell = albums[indexPath.row]
albumNameTapped = albumNameFromCell.value(forKeyPath: "albumName") as! String
self.performSegue(withIdentifier: "albumCellTappedSegue", sender: self)
}
}
It looks like you can't load any view's content in viewDidLoad
. According to the Apple View Controller Programming Guide, "viewDidLoad
is called when the view controller’s content view (the top of its view hierarchy) is created and loaded from a storyboard." Basically, it's called when the view controller and its subviews are loaded, but not visible. ViewDidAppear
is where the view controller and its subviews are made visible. According to the Apple View Controller Programming Guide, "viewDidAppear
is called just after the view controller’s content view has been added to the app’s view hierarchy." In other words, the view controller and its subviews have been made visible.
I was able to solve my problem, although the answer to it doesn't answer this question. I added the answer to that problem as an edit to my question.