How to come back to previous scene and reload data? - swift

I have two Scenes on Storyboard : One to show a list of items with plus button on the Bar to go to another Scene that has form to add a new item. I'm saving on Local Storage, in the saving function I want to come back to the previous page with the new data.
This is the saving function:
#IBAction func AddTrack(_ sender: Any) {
let item = TrackItem(context: PersistenceService.context)
item.kms = Int32(kmsField!.text!)!
item.liters = Float(litersField!.text!)!
item.date = textFieldPicker!.text!
PersistenceService.saveContext()
navigationController?.popViewController(animated: true)
self.TrackList.append(item)
self.tableView?.reloadData()
}
Knowing that I'm using this function in viewDidLoad() and viewDidAppear()
func GetData(){
let fetchRequest: NSFetchRequest<TrackItem> = NSFetchRequest<TrackItem>(entityName: "TrackItem")
do{
let TrackList = try PersistenceService.context.fetch(fetchRequest)
self.TrackList = TrackList
self.tableView?.reloadData()
}catch{
}
}

You can use NotificationCenter or Delegation (Depends on what you are trying to achieve)
Example NotificationCenter:
VC1, takes a photo:
buttonTapped / function () {
// Upload a Photo()
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "load"), object: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.dismiss(animated: true, completion: nil)
}
}
VC2, shows all photos:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(loadList), name: NSNotification.Name(rawValue: "load"), object: nil)
}
#objc func loadList(notification: NSNotification) {
//get data, reload data, etc
}

Related

Swift NSNotification Observers not working

I have 2 view controllers, one with a switch that when toggled should post the following notification. In the other view controller I have Observers which should trigger the following function which just toggles a boolean. I am not able to get the observers to work and make that function call, am I doing something wrong here? I have another Notification (Doesn't trigger with user input) that is being sent in the opposite direction which works fine.
#IBAction func switchAction(_ sender: Any) {
if switchUI.isOn {
print("Collecting Data ")
NotificationCenter.default.post(name:NSNotification.Name(rawValue: "Collect"), object: self)
}
else
{
print("Not Collecting Data")
NotificationCenter.default.post(name:NSNotification.Name(rawValue: "Do Not Collect"), object: self)
}
}
func collectDataObserver () {
//Add an Observer
NotificationCenter.default.addObserver(self, selector: #selector(CentralViewController.toggleData(notification:)), name: Notification.Name(rawValue: "Collect"), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(CentralViewController.toggleData(notification:)), name: Notification.Name(rawValue: "Do Not Collect"), object: nil)
}
#objc func toggleData(notification: NSNotification) {
let isCollectData = notification.name.rawValue == "Collect"
if(isCollectData){
IsCollectingData = true
}
else
{
IsCollectingData = false
}
}
You need to call collectDataObserver() in viewDidLoad() of CentralViewController, i.e.
override func viewDidLoad() {
super.viewDidLoad()
collectDataObserver()
}

How can I show same video as a background in all View Controllers without any interruption during the pages transition?

There are 2 pages(ViewController) in Main.storyboard: HomeViewController and DetailViewController
I gave a storyboard ID for the DetailViewController:DetailPage
I have added a button into first page to take user to the second page.
I also have added a back button into second page to take user to the first page back.
I need to show a video in the background in all pages.
So I have added an UIView component in all pages to show a video.
I gave 0,0,0,0 constraint values for these 2 UIView components in each pages.
First let me share source codes and then I would like to ask my questions.
HomeViewController.swift file
import UIKit
import AVFoundation
class HomeViewController: UIViewController {
private var player: AVPlayer!
#IBOutlet weak var outlet4TheVideoUiView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
configureVideoIssue()
}
override func viewWillAppear(_ animated: Bool) {
configureVideoIssue()
}
func configureVideoIssue()
{
// BACKGROUND VIDEO SCOPE STARTS
let path = URL(fileURLWithPath: Bundle.main.path(forResource: "clouds", ofType: "mp4")!)
let player = AVPlayer(url: path)
self.player = player
let newLayer = AVPlayerLayer(player: player)
newLayer.frame = self.view.bounds
outlet4TheVideoUiView.frame = self.view.bounds
outlet4TheVideoUiView.layer.addSublayer(newLayer)
newLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
player.play()
// video bitince tekrar oynatmak için
player.actionAtItemEnd = AVPlayer.ActionAtItemEnd.none
NotificationCenter.default.addObserver(self, selector: #selector(self.videoDidPlayToEnd(notification:)),
name: NSNotification.Name(rawValue: "AVPlayerItemDidPlayToEndTimeNotification"), object: player.currentItem)
NotificationCenter.default.addObserver(self, selector: #selector(enteredBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(enteredForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
// BACKGROUND VIDEO SCOPE ENDS
}
#objc func enteredBackground() {
player.pause()
}
#objc func enteredForeground() {
player.play()
}
#objc func videoDidPlayToEnd(notification: Notification)
{
let player: AVPlayerItem = notification.object as! AVPlayerItem
player.seek(to: .zero, completionHandler: nil)
}
#IBAction func linkBtnClick(_ sender: UIButton) {
let controller = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "DetailPage") as! DetailViewController
controller.modalPresentationStyle = .fullScreen
//present(controller, animated: true, completion: nil)
show(controller, sender: nil)
}
}
DetailViewController.swift file
import UIKit
import AVFoundation
class DetailViewController: UIViewController {
private var player: AVPlayer!
#IBOutlet weak var outlet4TheVideoUiView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
configureVideoIssue()
// Do any additional setup after loading the view.
}
func configureVideoIssue()
{
// BACKGROUND VIDEO SCOPE STARTS
let path = URL(fileURLWithPath: Bundle.main.path(forResource: "clouds", ofType: "mp4")!)
let player = AVPlayer(url: path)
self.player = player
let newLayer = AVPlayerLayer(player: player)
newLayer.frame = self.view.bounds
outlet4TheVideoUiView.frame = self.view.bounds
outlet4TheVideoUiView.layer.addSublayer(newLayer)
newLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
player.play()
// video bitince tekrar oynatmak için
player.actionAtItemEnd = AVPlayer.ActionAtItemEnd.none
NotificationCenter.default.addObserver(self, selector: #selector(self.videoDidPlayToEnd(notification:)),
name: NSNotification.Name(rawValue: "AVPlayerItemDidPlayToEndTimeNotification"), object: player.currentItem)
NotificationCenter.default.addObserver(self, selector: #selector(enteredBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(enteredForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
// BACKGROUND VIDEO SCOPE ENDS
}
#objc func enteredBackground() {
player.pause()
}
#objc func enteredForeground() {
player.play()
}
#objc func videoDidPlayToEnd(notification: Notification)
{
let player: AVPlayerItem = notification.object as! AVPlayerItem
player.seek(to: .zero, completionHandler: nil)
}
#IBAction func btnBackClick(_ sender: UIButton) {
dismiss(animated: true, completion: nil)
}
}
First Question: I am sure that it shouldn't be like this because same codes are written in both ViewControllers. So how can I avoid this? Any suggestions? I should use same video ui view object as a background for all view controllers. Does it make sense? If it does not make sense. What should I do? For example, I use asp.net user control component in Visual Studio for this scenario. I create one video component(user control) and I can use this component for all pages as a background.
Second Question: (if we can't find a solution to the first question)Let's assume that app user sees Homepage right now. And let's assume that user clicks button to see secondpage(detail page)after 10th second. Button click action takes user to the second page but video starts from the first second. Video should continue from 10th second or 11th second. How can I do this?
Third Question: Second page comes from the bottom after i click button in first page. Can second page comes with fading first page out animation and appearing second page animation?
Important details: Video should stop after app is minimized and should continue after app is maximized by app user. And other important detail is: buttons are not inside of the video view. User can see buttons above the video with this way. Let me show layers for the components in order with an image:
One way that you can do this is set your view on the rootWindow and then set all your views' background color to clear.
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
let window=self.window!
backgroundVideoView.bounds = window.bounds
window.addSubview(backgroundVideoView)
playVideo()
return true
}
Here, backgroundVideoView refers to the view which will be playing your video.
Although, I must warn you, this will consume a lot of memory but will get you the desired behavior.

How to unit test if a view controller has subscribed to a notification in swift

I would like to test if MyViewController has subscribed to notifications in the default NSNotificationCenter in swift.
Here is code you can drop right into a Playground. Inline comments explain what's going on.
import UIKit
import XCTest
//this is a useful helper function for delaying an action
func delay(_ delay:Double, closure:#escaping ()->()) {
//credit: Matt Neuberg - http://stackoverflow.com/questions/24034544/dispatch-after-gcd-in-swift/24318861#24318861
DispatchQueue.main.asyncAfter(
deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure)
}
class MyViewController : UIViewController {
//you just need something you can check to verify the notification was received
var didReceiveMyNotification = false
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "myNotification"), object: nil, queue: nil) {
notification in
//keep track when the notification is received
self.didReceiveMyNotification = true
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
class testMyViewController: XCTestCase {
func testShouldSubscribeToNotification() {
weak var expectation = self.expectation(description: "testShouldSubscribeToNotification")
//if your view controller is in a storyboard, then do this instead
//let viewController = UIStoryboard(name: "MyStoryboard", bundle: Bundle.main).instantiateViewController(withIdentifier: "MyViewControllerIdentifier") as! MyViewController
let viewController = MyViewController()
//force the view to load
let _ = viewController.view
//post the notification
NotificationCenter.default.post(name: Notification.Name(rawValue: "myNotification"), object: nil, userInfo: nil)
//wait however long is needed for the notification to get handled by view controller
delay(0.5) {
print("result=\(viewController.didReceiveMyNotification)")
//if the view controller was properly subscribed, this should be true
XCTAssertTrue(viewController.didReceiveMyNotification)
expectation?.fulfill()
}
//wait for the notification to get handled before allowing the test to exit
waitForExpectations(timeout: 1.0, handler: nil)
}
}
//if you want to run the test in a Playground...
let myTester = testMyViewController()
myTester.testShouldSubscribeToNotification()

Swift: Network-Request at AppStart in AppDelegate - CompletionHandler in ViewController?

my app is based on a TabBarController with 4 ViewControllers. All 4 of them are dependent of the same data. This is why I would like to load the data at the App start in the AppDelegate.
However, how does the ViewController know, that the request is completed? For example, if there is an error (e.g. no internet connection), how do I pass this error to any of these 4 ViewController to present an alert?
Use NotificationCenter to implement (Swift 3 code):
extension Notification.Name {
static var RequestCompleted = Notification.Name(rawValue: "MyRequestIsCompleted")
static var RequestError = Notification.Name(rawValue: "MyRequestError")
}
class DataRequest {
func request() {
// if there is error:
let error = NSError(domain: "Error Sample", code: 0, userInfo: nil)
NotificationCenter.default.post(name: .RequestError, object: error)
// if the request is completed
let data = "your data is here"
NotificationCenter.default.post(name: .RequestCompleted, object: data)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(requestCompleted(_:)), name: .RequestCompleted, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(requestError(_:)), name: .RequestError, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
func requestCompleted(_ notification: Notification) {
if let obj = notification.object {
print(obj)
}
}
func requestError(_ notification: Notification) {
if let obj = notification.object {
print(obj)
}
}
}

How can I move the whole view up when the keyboard pop up? (Swift)

The gray box at the bottom is a text view. When I tap the text view, the keyboard will pop up from bottom. However, the text view has been covered by the pop up keyboard.
What functions should I add in order to move up the whole view when the keyboard pops up?
To detect when a keyboard shows up you could listen to the NSNotificationCenter
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: “keyboardWillHide:", name: UIKeyboardWillHideNotification, object: nil)
That will call keyboardWillShow and keyboardWillHide. Here you can do what you want with your UITextfield
func keyboardWillShow(notification: NSNotification) {
if let userInfo = notification.userInfo {
if let keyboardSize = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
//use keyboardSize.height to determine the height of the keyboard and set the height of your textfield accordingly
}
}
}
func keyboardWillHide(notification: NSNotification) {
//pull everything down again
}
As Milo says, to do this yourself you register for for keyboard show/hide notifications.
You then need to write code that figures out how much of the screen the keyboard is hiding, and how high on the screen the field in question is located, so you know how much to shift your views.
Once you've done that what you do depends on whether you're using AutoLayout or autoresizing masks (a.k.a. "Struts and springs" style layout.)
I wrote a developer blog post about a project that includes working code for shifting the keyboard. See this link:
http://wareto.com/animating-shapes-using-cashapelayer-and-cabasicanimation
In that post, look for the link titled "Sliding your views to make room for the keyboard" at the bottom.
Swift 4 complete solution.
I use this in all of my projects that need it.
on viewWillAppear register to listen for the keyboard showing/hiding and use the functions below to move the view up and back down.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
subscribeToKeyboardNotifications()
}
// stop listening for changes when view is dissappearing
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
unsubscribeFromKeyboardNotifications()
}
// listen for keyboard show/show events
func subscribeToKeyboardNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow(_:)), name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide(_:)), name: .UIKeyboardWillHide, object: nil)
}
func unsubscribeFromKeyboardNotifications() {
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillHide, object: nil)
}
#objc func keyboardWillHide(_ notification: Notification) {
view.frame.origin.y = 0
}
In my case I have a text field at the bottom which gets hidden by the keyboard so if this is in use then I move the view up
#objc func keyboardWillShow(_ notification: Notification) {
if bottomTextField.isFirstResponder {
view.frame.origin.y = -getKeyboardHeight(notification: notification)
}
}
func getKeyboardHeight(notification: Notification) -> CGFloat {
let userInfo = notification.userInfo
let keyboardSize = userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue
return keyboardSize.cgRectValue.height
}
Swift 4
This code is not so perfect but worth a try!
First embed the whole view in a scrollView.
Add this little cute function to your class:
#objc func keyboardNotification(_ notification: Notification) {
if let userInfo = (notification as NSNotification).userInfo {
let endFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
let duration:TimeInterval = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0
let animationCurveRawNSN = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber
let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIViewAnimationOptions().rawValue
let animationCurve:UIViewAnimationOptions = UIViewAnimationOptions(rawValue: animationCurveRaw)
if (endFrame?.origin.y)! >= UIScreen.main.bounds.size.height {
scrollViewBottomConstraint?.constant = 0
} else {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
let keyboardHeight:Int = Int(keyboardSize.height)
scrollViewBottomConstraint?.constant = CGFloat(keyboardHeight)
scrollView.setContentOffset(CGPoint(x: 0, y: (scrollViewBottomConstraint?.constant)! / 2), animated: true)
}
}
UIView.animate(withDuration: duration, delay: TimeInterval(0), options: animationCurve, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
}
.
Add this one to your ViewDidLoad:
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardNotification(_:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
Don't forget to connect bottom constraint of your scrollView to your class (with name: "scrollViewBottomConstraint").