How to Build A Dynamic UIPageViewController - ios

I have followed the basic tutorial here to create a UIPageViewController that swipes through different ViewControllers, all of which I create on Main.Storyboard. I want to make an application that generates dynamic ViewControllers, meaning I will have to instantiate the ViewControllers in the UIPageViewController programatically.
However, I am trying to take everything a bit further. I am going to have a dynamic set of ViewControllers to display based on data I have in Firebase. On each swipe, I want to instantiate a view that is going to contain an image (the image will be stored on Amazon Web Services, and a link to the image will be stored in Firebase). I currently have this setup in my Android application and use Picasso to load the image in my fragment.
Since all of the data is going to be dynamic, I want to find the best way to:
-Instantiate new ViewControllers based on dynamic firebase data that changes each day
-Have each ViewController display the associated image I have stored in Firebase
All of the ViewControllers will have the same "skeleton," meaning each one will have an image accompanied by some text. Below is my current code, and I was hoping to receive some advice / assistance.
import UIKit
class PageViewTutorial: UIPageViewController {
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
if let firstViewController = orderedViewControllers.first {
setViewControllers([firstViewController],
direction: .Forward,
animated: true,
completion: nil)
}
}
//this will be dynamic based on firebase data
private(set) lazy var orderedViewControllers: [UIViewController] = {
return [self.newColoredViewController("Green"),
self.newColoredViewController("Red"),
self.newColoredViewController("Blue")]
}()
private func newColoredViewController(color: String) -> UIViewController {
return UIStoryboard(name: "Main", bundle: nil) .
instantiateViewControllerWithIdentifier("\(color)ViewController")
}
}
// MARK: PageViewTutorialDataSource
extension PageViewTutorial: UIPageViewControllerDataSource {
func pageViewController(pageViewController: UIPageViewController,
viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.indexOf(viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
return nil
}
guard orderedViewControllers.count > previousIndex else {
return nil
}
return orderedViewControllers[previousIndex]
}
func pageViewController(pageViewController: UIPageViewController,
viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.indexOf(viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
let orderedViewControllersCount = orderedViewControllers.count
guard orderedViewControllersCount != nextIndex else {
return nil
}
guard orderedViewControllersCount > nextIndex else {
return nil
}
return orderedViewControllers[nextIndex]
}
}

After new comments I would do it this way:
Make some Poll object with variables text, images, id, day an so on.
Make an PollViewModel initialized with Poll object, that will be respond to data presentation
Make an UIViewController/UITableViewController subclass initialized with PollViewModel
Make an subclasses of UITableViewCell for every part of the poll UI, that can beand display it in UITableView
Make a PageViewTutorial class (or its own ViewModel) initialized with current array of Poll objects from FireBase
In UIPageViewControllerDataSource handle objects from array and return vc initialized with model initialized with with poll object. ;)

Related

how to unit test UIApplication extension

Suppose I use this code that extracts the top most viewController
import UIKit
extension UIApplication {
class func topViewController(_ base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = base as? UINavigationController, navigationController.viewControllers.count > 0 {
return topViewController(navigationController.visibleViewController)
}
if let tabBarController = base as? UITabBarController {
if let selected = tabBarController.selectedViewController {
return topViewController(selected)
}
}
if let presentedViewController = base?.presentedViewController {
return topViewController(presentedViewController)
}
return base
}
}
How do I facilitate unit testing of this code? I would need to use an instance of UIApplication.shared. Any tips would be appreciated.
If instead this was an extension to UIViewController, you could omit the parameter (base) altogether.
The call would then change to
let top = UIApplication.shared.keyWindow?.rootViewController.topViewController()
In order to unit test this, we can simply create a ViewController and perform our tests.

What is the proper way to get root view controller in iOS 13?

class TopViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//Code Block 1
let controller = getTopController()
print(controller)// Prints out MyTestProject.TopViewController
//Code Block 2
let controller2 = getRootController()
print(controller2)//Prints out nil , because keywindow is also nil upto this point.
//Code Block 3
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) {
let controller2 = self.getRootController()
print(controller2)// Prints out MyTestProject.TopViewController
}
}
func getTopController() -> UIViewController? {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let sceneDelegate = windowScene.delegate as? SceneDelegate else {
return nil
}
return sceneDelegate.window?.rootViewController
}
func getRootController() -> UIViewController? {
let keyWindow = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
let topController = keyWindow?.rootViewController
return topController
}
}
Since iOS 13 there is two approach to get current active / top view controller of the app.
here:
getTopController() and getRootController() shows both of the approaches.
As commented in codes besides print() results are different though.
In Code Block 2:
getRootController can't find the window yet so it prints out nil. Why is this happening?
Also, which is the full proof method of getting reference to top controller in iOS 13, I am confused now?
The problem is that when your view controller viewDidLoad window.makeKey() has not been called yet.
A possible workaround is to get the first window in the windows array if a key window is not available.
func getRootController() -> UIViewController? {
let keyWindow = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) ?? UIApplication.shared.windows.first
let topController = keyWindow?.rootViewController
return topController
}
Please note that this will solve your problem but you should postpone any operation that involve using a key window until it is such.
According to the documentation of UIView, the window property is nil if the view has not yet been added to a window which is the case when viewDidLoad is called.
Try to access that in viewDidAppear
override func viewDidAppear(_ animated: Bool) {
let controller2 = self.view.window.rootViewController
}

How to generate additional view controllers in multi-page UI iOS app (horizontal carousel) on demand in Swift 4?

I've made a simple weather app as a side project (I'm not a software engineer) that shows temperatures in Fahrenheit and Celsius at the same time. The user can swipe horizontally between cities:
I've achieved this by using the multi-page app template that was available in Xcode as a new app template up until Xcode 9. When building the app up until Xcode 10, the problem was that when the user added a city to the array of cities, pages for newly added cities would not render if the user was on the last page of the carousel at the time of adding a new city. Even if so, when the user swiped left and then back right, the newly added cities would show up. Now, in Xcode 11, new view controllers for newly added cities never render, unless the user relaunches the app. That's a pretty big issue I'd like to fix.
I need help in figuring out how new view controllers can be rendered on demand into the existing multi-page carousel after addCity is called, even if the user is currently on the last page and w/o requiring an app restart.
Below is the full code that drives that multi-page carousel. Apologies some of the hacky stuff in there. For context, I'm a non-technical product manager trying to make some iOS apps as side projects.
import UIKit
import StoreKit
/*
A controller object that manages a simple model
The controller serves as the data source for the page view controller; it therefore implements pageViewController:viewControllerBeforeViewController: and pageViewController:viewControllerAfterViewController:.
It also implements a custom method, viewControllerAtIndex: which is useful in the implementation of the data source methods, and in the initial configuration of the application.
There is no need to actually create view controllers for each page in advance -- indeed doing so incurs unnecessary overhead. Given the data model, these methods create, configure, and return a new view controller on demand.
*/
class ModelController: NSObject, UIPageViewControllerDataSource {
var rootViewController = RootViewController()
var cities = [""]
let defaults = UserDefaults.standard
let currentCount = UserDefaults.standard.integer(forKey: "launchCount")
var pageViewController: UIPageViewController?
override init() {
super.init()
self.cities = self.defaults.stringArray(forKey: "SavedStringArray") ?? [String]()
if self.cities == [""] || self.cities.count == 0 {
self.cities = ["Current Location", "San Francisco", "New York"]
}
}
func addCity(name:String) {
self.cities.append(name)
self.defaults.set(self.cities, forKey: "SavedStringArray")
}
func viewControllerAtIndex(_ index: Int, storyboard: UIStoryboard) -> DataViewController? {
// Return the data view controller for the given index.
if (self.cities.count == 0) || (index >= self.cities.count) {
return nil
}
// Create a new view controller and pass suitable data.
let dataViewController = storyboard.instantiateViewController(withIdentifier: "DataViewController") as! DataViewController
//get city name
dataViewController.dataObject = self.cities[index]
return dataViewController
}
func indexOfViewController(_ viewController: DataViewController) -> Int {
// Return the index of the given data view controller.
// For simplicity, this implementation uses a static array of model objects and the view controller stores the model object; you can therefore use the model object to identify the index.
return self.cities.firstIndex(of: viewController.dataObject) ?? NSNotFound
}
// MARK: - Page View Controller Data Source
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
var index = self.indexOfViewController(viewController as! DataViewController)
if (index == 0) || (index == NSNotFound) {
return nil
}
index -= 1
return self.viewControllerAtIndex(index, storyboard: viewController.storyboard!)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
var index = self.indexOfViewController(viewController as! DataViewController)
if index == NSNotFound {
return nil
}
index += 1
if index == self.cities.count {
return nil
}
if index > 1 && currentCount > 2 {
print ("request review")
SKStoreReviewController.requestReview()
}
return self.viewControllerAtIndex(index, storyboard: viewController.storyboard!)
}
}

Cache videos in view controller using plugin

I started learning swift a few weeks ago and I am now moving on to something a little trickier: I would like to build a simple app which uses a UIPageViewController. Each page shall contain a video which is loaded from a server using a plugin called Player. Since I want individual videos to be player on the pages, I use an array which stores all the URLs. These URLs are then used to set a value in another class. If the value in this class was set (didSet), I load the video using the plugin. All that works perfectly fine! To get an idea of what I did you can click this link to this youtube tutorial which I used as an orientation.
However, I encountered a problem which I had already suspected: The videos are reloaded whenever one hits a page one has already been on and the video has already been loaded on once. Obviously, I don't want the videos to reload every time which is why I looked for a caching library for swift under awesome-swift. The first one suggested (HanekeSwift) somehow didn't work for me and gave me a bunch of errors every time I tried to include it. Thus, I tried to approach my problem using the second option provided: Carlos which didn't cause any errors (which is a library by a German news agency so it sounds and also looks very professional). Nevertheless, as I am quite new to swift, I cannot quite put my finger on this plugin. I don't understand a lot of things since I am quite new...
What would I have to add to my code to cache the videos? Do I need to create another class for that? How would you recommend me to do it? I'll provide what i have till now below... And sorry for the long (and newby) question but I really couldn't figure it out.
Code
PageViewController
//viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
//set datasourcee to self for now
dataSource = self
view.backgroundColor = .white
let postViewController = PostViewController()
postViewController.video = videos.first
let viewControllers = [postViewController]
//set first view controller
setViewControllers(viewControllers, direction: .forward, animated: true, completion: nil)
}
//videos array
let videos = ["https://link.to/testvideo.mov", "https://link.to/testvideo.mov", "https://link.to/testvideo.mov", "https://link.to/testvideo.mov"]
//after page
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
let currentIndexString = (viewController as! PostViewController).index
let currentIndex = indec.index(of: currentIndexString!)
//next page possible?
if currentIndex! < indec.count - 1 {
let postViewController = PostViewController()
postViewController.video = videos[currentIndex! + 1]
return postViewController
}
return nil
}
//before page
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
let currentIndexString = (viewController as! PostViewController).index
let currentIndex = indec.index(of: currentIndexString!)
//previous page possible?
if currentIndex! > 0 {
//template
let postViewController = PostViewController()
postViewController.video = videos[currentIndex! - 1]
return postViewController
}
return nil
}
PostViewController
var video:String? {
didSet {
print(video ?? String())
setupPlayer()
}
}
//orienting myself by the example provided by "Player" plugin from here on (https://github.com/piemonte/Player/blob/master/Project/Player/ViewController.swift)
fileprivate var player = Player()
deinit {
self.player.willMove(toParentViewController: self)
self.player.view.removeFromSuperview()
self.player.removeFromParentViewController()
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .blue
}
func setupPlayer() {
self.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.player.playerDelegate = self as? PlayerDelegate
self.player.playbackDelegate = self as? PlayerPlaybackDelegate
self.player.view.frame = self.view.bounds
self.addChildViewController(self.player)
self.view.addSubview(self.player.view)
self.player.didMove(toParentViewController: self)
self.player.url = URL(string: video!)
self.player.playbackLoops = true
}

How to randomize page order

I'm working on a quote app, I want the pages, except the first one, to be randomized. I don't want people opening the app to see the same initial quotes over and over again. If you could respond with edit to the code I would really appreciate it. Thank you in advance!
import UIKit
class ModelController: NSObject, UIPageViewControllerDataSource {
let pageData:NSArray = ["All quotes are from Kanye West, Enjoy!", "I refuse to accept other people's ideas of happiness for me. As if there's a 'one size fits all' standard for happiness","I am the greatest","I still think I'm the greatest.","They say you can rap about anything except for Jesus, that means guns, sex, lies, video tapes, but if I talk about God my record won't get played Huh?"]
override init() {
super.init()
}
func viewControllerAtIndex(_ index: Int, storyboard: UIStoryboard) -> DataViewController? {
// Return the data view controller for the given index.
if (self.pageData.count == 0) || (index >= self.pageData.count) {
return nil
}
// Create a new view controller and pass suitable data.
let dataViewController = storyboard.instantiateViewController(withIdentifier: "DataViewController") as! DataViewController
dataViewController.dataObject = self.pageData[index] as! String
return dataViewController
}
func indexOfViewController(_ viewController: DataViewController) -> Int {
// Return the index of the given data view controller.
// For simplicity, this implementation uses a static array of model objects and the view controller stores the model object; you can therefore use the model object to identify the index.
return pageData.index(of: viewController.dataObject)
}
// MARK: - Page View Controller Data Source
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
var index = self.indexOfViewController(viewController as! DataViewController)
if (index == 0) || (index == NSNotFound) {
return nil
}
index -= 1
return self.viewControllerAtIndex(index, storyboard: viewController.storyboard!)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
var index = self.indexOfViewController(viewController as! DataViewController)
if index == NSNotFound {
return nil
}
index += 1
if index == self.pageData.count {
return nil
}
return self.viewControllerAtIndex(index, storyboard: viewController.storyboard!)
You can randomise your quote array and pass on the randomised array.
Checkout these link:
How do I shuffle an array in Swift? and
Shuffle array swift 3

Resources