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
}
Related
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
}
I want the user to be able to know how many times they have visited each class. Then add together the totals from each page together to form a group sum. I want to print the total sum in the log file in each of the two view controllers. So just one string should be printed.
class oneV: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
UserDefaults.standard.set(true, forKey: "VC1")
}
}
class twoV: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
UserDefaults.standard.set(true, forKey: "VC2")
}
}
If you mean visited each view controller, when you say visited each class. Then i'd recommend you do it viewDidAppear.
class YourViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let key = String(describing: type(of: self))
let count = UserDefaults.standard.value(forKey: key) as? Int ?? 0
UserDefaults.standard.set(value + 1, forKey: key)
}
}
To make it simpler, you could use an extension on UIViewController.
extension UIViewController {
func updateVisitCount() {
let key = String(describing: type(of: self))
let count = UserDefaults.standard.value(forKey: key) as? Int ?? 0
UserDefaults.standard.set(count + 1, forKey: key)
}
}
Or, if you need this for every view controller that you create, then you can create a base view controller which you would use everywhere instead of UIViewController.
class BaseViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateVisitCount()
}
}
The most automatic solution would be inject the accounting call in viewDidLoad without replacing the original viewDidLoad.
Here demo purpose i've created a sample Playground
import UIKit
import PlaygroundSupport
extension UIViewController {
#objc dynamic func substitutedViewDidAppear() {
print("This is injected code in view did appear")
substitutedViewDidAppear() // it may look like recursive, but it isn't, actually it calls the original `viewDidAppear` method.
}
class func swizzle() {
let originalMethod = class_getInstanceMethod(UIViewController.self, #selector(viewDidAppear(_:)))
let substitutedMethod = class_getInstanceMethod(UIViewController.self, #selector(substitutedViewDidAppear))
if let originalMethod = originalMethod,
let substitutedMethod = substitutedMethod {
print("swizzled")
method_exchangeImplementations(originalMethod, substitutedMethod)
} else {
print("not swizzled")
}
}
}
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let label = UILabel()
label.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
label.text = "Hello World!"
label.textColor = .black
view.addSubview(label)
self.view = view
print("view loaded")
}
}
// Swizzle
UIViewController.swizzle() // call this in #top of didFinishLaunchingWithOptions
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
Output:
swizzled
view loaded
This is injected code in view did appear
Now in the substitutedViewDidAppear upper portion inject your counting code as #Rakesha Shastri Suggested, call the updateVisitCount method inside of substitutedViewDidAppear & place the UIViewController.swizzle() in applicationDidFinishLaunchingWithOptions before creating the root window.
Create a static variable. A static variable is a type of class, not object therefore throughout all objects a variable maybe maintained. I think this example may better explain how this works. Click here
In ViewDidLoad method call this function :
func updateVisitingCounter() {
var counter = UserDefaults.standard.integer(forKey: "firstPageCounter")
counter += 1
UserDefaults.standard.set(counter, forKey: "firstPageCounter")
}
You may set declare variables at project scope "outside of classes"
var vc1Count = 0
class oneV: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
vc1Count = vc1Count+1
}
}
var vc2Count = 0
class twoV: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
vc2Count = vc2Count+1
}
}
you can also declare these variables at a common place.
As per your requirements its kind of Analytics on app usage. You can implement in 2 ways
By storing data with screen visit in local DB and show it on Analysis Page or on summery page.
Sample code for storing Screen details in DB:
==> Create your Entity for Screen capture.
ScreenVisit.
==> Store Data with screen name.
let entity = NSEntityDescription.entity(forEntityName: "ScreenVisit", in: context)
let newVisit = NSManagedObject(entity: entity!, insertInto: context)
newVisit.setValue("HomeScreen", forKey: "screenname")
newVisit.setValue("1", forKey: "visited")
do {
try context.save()
} catch {
print("Failed saving")
}
==> Fetch data where you required.
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "ScreenVisit")
//request.predicate = NSPredicate(format: <Your Filter Logic>)
request.returnsObjectsAsFaults = false
do {
let result = try context.fetch(request)
for data in result as! [NSManagedObject] {
print(data.value(forKey: "screenname") as! String)
print(data.value(forKey: "visited") as! String)
}
} catch {
print("Failed")
}
You can use any 3rd party library like Google analytics, Crashlytics for tracking your user actions.
Ref Links :
Firebase iOS analytics
Crashlytics
but as per my experience 2nd way is more convenient and powerful.
All depends on your requirements.
Hope this will helps you to get your user action captured.
I know that there are multiple approaches to pass data back from one controller to another like Delegates, NSNotifications. I am using another way using Closures to pass data data back. I just want to know is it safe way how I pass any data using blocks like below or should I avoid this approach.
First ViewController (where I make object of Second ViewController)
#IBAction func push(sender: UIButton) {
let v2Obj = storyboard?.instantiateViewControllerWithIdentifier("v2ViewController") as! v2ViewController
v2Obj.completionBlock = {(dataReturned) -> ()in
//Data is returned **Do anything with it **
print(dataReturned)
}
navigationController?.pushViewController(v2Obj, animated: true)
}
Second ViewController (where data is passed back to First VC)
import UIKit
typealias v2CB = (infoToReturn :NSString) ->()
class v2ViewController: UIViewController {
var completionBlock:v2CB?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func returnFirstValue(sender: UIButton) {
guard let cb = completionBlock else {return}
cb(infoToReturn: returnFirstValue)
}
#IBAction func returnSecondValue(sender: UIButton) {
guard let cb = completionBlock else {return}
cb(infoToReturn: returnSecondValue)
}
}
That's a very good and reasonable approach and much better than notifications.
Looking at the evolution of Cocoa API you will notice that Apple has replaced more and more delegate API with blocks / closures over the years.
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. ;)
[new to swift] I testing this function to export some simple file
#IBAction func exportFile(delegate: UIDocumentInteractionControllerDelegate) {
print("export csv")
let fileName = tmpDir.stringByAppendingPathComponent("myFile.csv")
let url: NSURL! = NSURL(fileURLWithPath: fileName)
if url != nil {
let docController = UIDocumentInteractionController(URL: url)
docController.UTI = "public.comma-separated-values-text"
docController.delegate = delegate
docController.presentPreviewAnimated(true)
}
}
// Return the view controller from which the UIDocumentInteractionController will present itself.
func documentInteractionControllerViewControllerForPreview(controller: UIDocumentInteractionController)-> UIViewController {
return self
}
But when i clicked the export button i am getting the message
UIDocumentInteractionController delegate must implement documentInteractionControllerViewControllerForPreview: to allow preview
I thought
class ViewController: UIViewController, UIDocumentInteractionControllerDelegate {
Would be sufficient?
I tried
Self.documentInteractionControllerViewForPreview(docController)
[edit]
turned out i had made the following mistake
docController.delegate = self//delegate
You need to implement following delegate methods. Delegates are the callbacks from the service provider to service consumer to be prepared for the action that is about to occur. On some occasions you must provide details (called data source) in order to utilise the said functionality.
func documentInteractionControllerViewControllerForPreview(controller: UIDocumentInteractionController!) -> UIViewController! {
return self
}
func documentInteractionControllerViewForPreview(controller: UIDocumentInteractionController!) -> UIView! {
return self.view
}
func documentInteractionControllerRectForPreview(controller: UIDocumentInteractionController!) -> CGRect {
return self.view.frame
}
Read through how delegation works. Also, take a look at your specific case here.
For Swift 3.0
First extend your class
UIDocumentInteractionControllerDelegate
Then only implement the following method
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
return self
}
Calling the Pdf Viewer
func MyViewDocumentsmethod(){
let controladorDoc = UIDocumentInteractionController(url: PdfUrl! as URL)
controladorDoc.delegate = self
controladorDoc.presentPreview(animated: true)
}
This should render the following