I am writing a "notebook" style program. Notebooks have multiple pages and I am trying to put in a "go to page" popover to allow the user to go to any page. The popover presents a collectionView of thumbnails of each page. Obviously, it would be great to:
Click on desired page thumbnail in popover
Send the selectedPage via delegate
Have the recipient mainViewController display the selectedPage
The popover works great. The page selection works and passes the selectedPage back.
The problem is that I need to have the mainViewController "wait" for the user to select a page. Here the relevant sections of code in the mainViewController:
User selects barButton to show popover:
#IBAction func selectPage(sender: UIBarButtonItem) {
self.performSegueWithIdentifier("showPages", sender: self)
//need to wait here for the popover to be dismissed.
//the next line is executed before segue even appears
//"while" and delay don't work
imageView.image = currentNotebook.pages[goToPage! - 1] //displays selectedPage (goToPage is set by delegation)
goToPage = nil
}
//segue to popover
case "showPages" :
let navigationController = segue.destinationViewController as? PageCollectionViewController
if let vc = navigationController {
vc.delegate = self
vc.modalInPopover = false
vc.preferredContentSize = CGSizeMake(400,100)
vc.notebook = self.currentNotebook
print("got to show pages")
}
I suspect I need some sort of closure or handler in the selectPage function, but I can't figure it out. Hope that is clear enough. It is very early in the morning...
Here is the code for the popover:
import UIKit
protocol PageCollectionViewControllerDelegate {
func selectsPage(selectedPage:Int)
}
class PageCollectionViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
var notebook: Notebook!
var pageNum: Int!
var delegate: PageCollectionViewControllerDelegate?
#IBOutlet weak var pageCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
self.pencilCollectionView!.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func numberOfSectionsInCollectionView(pageCollectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return notebook.pages.count
}
func collectionView(pageCollectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = pageCollectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! PageCollectionViewCell
let cellImage = notebook.pages[indexPath.row]
let tempThumb = imageWithImage(cellImage, scaledToFillSize: CGSizeMake(cell.bounds.width, cell.bounds.height)) //create thumbnail of each page
cell.pageThumb.image = tempThumb
cell.backgroundColor = UIColor.whiteColor()
return cell
}
func collectionView(pageCollectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
//return page number
if let delegate = self.delegate {
pageNum = indexPath.row + 1
print("page number",pageNum)
delegate.selectsPage(pageNum)
}
self.dismissViewControllerAnimated(true, completion: nil) //if you comment this out the popover is not dismissed when clicking on a cell
}
func imageWithImage(image: UIImage, scaledToFillSize size: CGSize) -> UIImage {
let scale: CGFloat = max(size.width / image.size.width, size.height / image.size.height)
let width: CGFloat = image.size.width * scale
let height: CGFloat = image.size.height * scale
let imageRect: CGRect = CGRectMake((size.width - width) / 2.0, (size.height - height) / 2.0, width, height)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
image.drawInRect(imageRect)
let newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
}
This is a typical example of async programming. In this case, the async event you're waiting for is a user response.
The almost universal answer is "don't wait. Send a message and have the event notify you when it's done."
What I would do is create a custom subclass of UIViewController as your popover, and either give the popover a completion block (closure) property, or set it up with a delegate, and define a protocol so that the popover can notify it's delegate when the user selects an option. (The two approaches are quite similar, but with a completion block you don't have to define a method that gets invoked - you just pass in your completion code when you make the call that invokes the popover.)
Related
I have a paging UIScrollView, each page being populated with a different view controller. Above the scrollview is a UICollectionView that acts as a menu bar. As you scroll through the scrollview pages, the menu bar moves just a little bit. You can see from the gif on the left.
Setting their delegates to different classes keeps everything working correctly as seen in the gif on the left. BUT, setting them to the same class messes up the UICollectionViews behavior.
How do I set their delegates to the same class?
import UIKit
class MenuView: UIView, UICollectionViewDataSource {
let collcetionView: UICollectionView = {
let view = UICollectionView()
// Setup...
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupCollectionView()
collcetionView.dataSource = self
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate func setupCollectionView() {
// Autolayout code...
}
// Datasource methods to populate collection view cells
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// Populate cell code...
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// Populate cell code...
}
}
class MainView: UIView {
// Contains paging scroll view and menu bar
var menu: MenuView!
let scrollView: UIScrollView = {
let view = UIScrollView()
// Setup...
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupMenu()
setupScrollView()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate func setupScrollView() {
// Autolayout code...
}
fileprivate func setupMenu() {
menu = MenuView()
// Autolayout code...
}
}
class MainController: UIViewController, UIScrollViewDelegate, UICollectionViewDelegate {
var mainView: MainView!
override func loadView() {
super.loadView()
mainView = MainView()
view = mainView
}
override func viewDidLoad() {
super.viewDidLoad()
mainView.scrollView.delegate = self
mainView.menu.collcetionView.delegate = self // <<--- THIS IS WHAT BREAKS EVERYTHING
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// Moving menu bar with page scroll
mainView.menu.collectionView.contentOffset = CGPoint(x: scrollView.contentOffset.x/SCROLL_FACTOR - (firstIndexPosition/SCROLL_FACTOR - difference/2), y: 0)
// Fade in and out highlighted state of menu bar cell
let exactPage = (scrollView.contentOffset.x / SCREEN_WIDTH)
let currentPage = (scrollView.contentOffset.x / SCREEN_WIDTH).rounded()
let unitExact = currentPage - exactPage
//print(String(format: "exact: %.2f, ", exactPage) + "current: \(currentPage), " + String(format: "unit: %.2f, ", unitExact))
if exactPage > currentPage {
// exact > current
// fade out/in left icon
// select current
let unit = 0 - unitExact // from 0 - 0.5
let cell = mainView.menu.collectionView.cellForItem(at: IndexPath(item: Int(currentPage), section: 0)) as! MenuBarCell
let mapped = unit.map(from: 0.0...0.5, to: 0...149.0)
print(cell)
setCellColor(cell: cell, value: mapped)
} else if exactPage < currentPage {
// exact < current
// fade out/in right icon
// select current
let unit = unitExact // from 0 - 0.5
let cell = mainView.menu.collectionView.cellForItem(at: IndexPath(item: Int(currentPage), section: 0)) as! MenuBarCell
let mapped = unit.map(from: 0.0...0.5, to: 0...149.0)
setCellColor(cell: cell, value: mapped)
} else if exactPage == currentPage {
// exact = current
// darken that icon
// select current
}
}
}
UICollectionView and UITableView inherit from UIScrollView,
The scrollViewDidScroll delegate method will be called for both your collection view and your scrollview if you set the delegate for both objects to the same class.
You need to check why scrollViewDidScroll is being called and act accordingly.
The simplest approach is a guard statement that returns if the delegate method isn't called for the scroll view you are interested in.
If you needed to execute different code depending on the scroll view involved you could use a series of if statements or a switch statement.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView == self.scrollView else {
return
}
// Moving menu bar with page scroll
mainView.menu.collectionView.contentOffset = CGPoint(x: scrollView.contentOffset.x/SCROLL_FACTOR - (firstIndexPosition/SCROLL_FACTOR - difference/2), y: 0)
// Fade in and out highlighted state of menu bar cell
let exactPage = (scrollView.contentOffset.x / SCREEN_WIDTH)
let currentPage = (scrollView.contentOffset.x / SCREEN_WIDTH).rounded()
let unitExact = currentPage - exactPage
//print(String(format: "exact: %.2f, ", exactPage) + "current: \(currentPage), " + String(format: "unit: %.2f, ", unitExact))
if exactPage > currentPage {
// exact > current
// fade out/in left icon
// select current
let unit = 0 - unitExact // from 0 - 0.5
let cell = mainView.menu.collectionView.cellForItem(at: IndexPath(item: Int(currentPage), section: 0)) as! MenuBarCell
let mapped = unit.map(from: 0.0...0.5, to: 0...149.0)
print(cell)
setCellColor(cell: cell, value: mapped)
} else if exactPage < currentPage {
// exact < current
// fade out/in right icon
// select current
let unit = unitExact // from 0 - 0.5
let cell = mainView.menu.collectionView.cellForItem(at: IndexPath(item: Int(currentPage), section: 0)) as! MenuBarCell
let mapped = unit.map(from: 0.0...0.5, to: 0...149.0)
setCellColor(cell: cell, value: mapped)
} else if exactPage == currentPage {
// exact = current
// darken that icon
// select current
}
}
I am wondering for quiet a while now how to create the iOS Photos App layout. How can I make it so it looks like zooming in to a collection while at the same time the navigation bar shows a back button?
Is it a new view controller which gets pushed onto a UINavigationController? And if so, how exactly do they manage to match the tiles while expanding.
Is there maybe even a 3rd party library which lets me easily recreate such a layout?
Hope you can help me to understand the concept of how this works.
To answer your first question, "Is it a new view controller which gets pushed onto a UINavigationController?". Yes, it is a new view controller. What Apple is using here is a UIViewControllerTransitioningDelegate which allows you to present a custom animation on how a view controller is presented and dismissed.
Now on the second question, "Hope you can help me to understand the concept of how this works." There is no easy way to put it as quite a lot is involved. I have recreated the effect which I will show below but first I need to explain some core principles.
From Apple's docs,
When implementing your transitioning delegate object, you can return different animator objects depending on whether a view controller is being presented or dismissed. All transitions use a transition animator object—an object that conforms to the UIViewControllerAnimatedTransitioning protocol—to implement the basic animations. A transition animator object performs a set of animations over a finite period of time.
In other words, the UIViewControllerTransitioningDelegate expects an animator object which you create that describes how the view controller should be presented and how it should be dismissed. Only two of these delegates methods are of interest to what you want to achieve and these are:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = PresentAnimator()
return animator
}
This asks your delegate for the transition animator object to use when presenting a view controller.
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = DismissAnimator()
return animator
}
This asks your delegate for the transition animator object to use when dismissing a view controller.
Both the PresentAnimator and DismissAnimator object conform to UIViewControllerAnimatedTransitioning. From Apple's docs:
In your animator object, implement the transitionDuration(using:) method to specify the duration of your transition and implement the animateTransition(using:) method to create the animations themselves. Information about the objects involved in the transition is passed to your animateTransition(using:) method in the form of a context object. Use the information provided by that object to move the target view controller’s view on or off screen over the specified duration.
Basically, each animator object will describe the duration of the view controller's animation and how it will be animated.
Now here is a demonstration of all this. This is what we will achieve:
Create two view controllers in your storyboard. My first view controller is called ViewController which contains a Collection View and a Collection View cell with an identifier "MediaCell" and an image that fills that collection view cell. The collection view cell has a class called ImageCollectionViewCell with only this:
class ImageCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var image: UIImageView! //links to the collection view cell's image
}
My second view controller is called ImageRevealViewController which simply has a single image view and a grey view at the top that I am using to simulate a navigation bar and a custom back button (I have tried all this with a normal UINavigationController nav bar but the dismiss animator fails to work. There is no shame through is making something that looks and acts like a navigation bar although mine is just for demo).
The Photo Album
This will be the code for your ViewController. Basically this will be the place the user finds a collection of photos just like the Photo Album. I used two test images for mine as you will see.
import UIKit
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
#IBOutlet weak var collectionView: UICollectionView!
var selectedCell = UICollectionViewCell() //the selected cell, important for the animator
var media: [UIImage] = [UIImage]() //the photo album's images
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
media.append(UIImage(named: "testimage1")!)
media.append(UIImage(named: "testimage2")!)
collectionView.delegate = self
collectionView.dataSource = self
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedCell = collectionView.cellForItem(at: indexPath)!
let selectedCellImage = selectedCell as! ImageCollectionViewCell
let mainStoryboard = UIStoryboard(name: "Main", bundle: nil)
let imageRevealVC = mainStoryboard.instantiateViewController(withIdentifier: "ImageRevealVC") as! ImageRevealViewController
imageRevealVC.transitioningDelegate = self
imageRevealVC.imageToReveal = selectedCellImage.image.image
/*
This is where I tried using the nav controller but things did not work out for the dismiss animator. I have commented it out.
*/
//let navController = UINavigationController(rootViewController: imageRevealVC)
//navController.transitioningDelegate = self
//navigationController?.pushViewController(imageRevealVC, animated: true)
present(imageRevealVC, animated: true, completion: nil)
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return media.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MediaCell", for: indexPath) as! ImageCollectionViewCell
cell.image.image = media[indexPath.row]
cell.image.contentMode = .scaleAspectFill
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let itemsPerRow:CGFloat = 3
let hardCodedPadding:CGFloat = 2
let itemWidth = (collectionView.bounds.width / itemsPerRow) - hardCodedPadding
let itemHeight = itemWidth
return CGSize(width: itemWidth, height: itemHeight)
}
}
extension ViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = PresentAnimator()
animator.originFrame = selectedCell.frame //the selected cell gives us the frame origin for the reveal animation
return animator
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = DismissAnimator()
return animator
}
}
The UIViewControllerTransitioningDelegate is at the end alongside the animator objects I talked about. Notice in the didSelect of the collection view that I instantiate the new view controller and make its transitioning delegate equal to self.
The Animators
There are always three steps to making an animator.
Setup the transition
Create the animations
Complete the transitions
Now for the Present Animator. Create a new Swift class called PresentAnimator and add the following:
import Foundation
import UIKit
class PresentAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let duration = 0.5
var originFrame = CGRect.zero
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)!
//2) create animation
let finalFrame = toView.frame
let xScaleFactor = originFrame.width / finalFrame.width
let yScaleFactor = originFrame.height / finalFrame.height
let scaleTransform = CGAffineTransform(scaleX: xScaleFactor, y: yScaleFactor)
toView.transform = scaleTransform
toView.center = CGPoint(
x: originFrame.midX,
y: originFrame.midY
)
toView.clipsToBounds = true
containerView.addSubview(toView)
UIView.animate(withDuration: duration, delay: 0.0,
options: [], animations: {
toView.transform = CGAffineTransform.identity
toView.center = CGPoint(
x: finalFrame.midX,
y: finalFrame.midY
)
}, completion: {_ in
//3 complete the transition
transitionContext.completeTransition(
!transitionContext.transitionWasCancelled
)
})
}
}
Now for the Dismiss Animator. Create a new class called DismissAnimator and add the following:
import Foundation
import UIKit
class DismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let duration = 0.5
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
//1) setup the transition
let containerView = transitionContext.containerView
let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)!
let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)!
containerView.insertSubview(toView, belowSubview: fromView)
//2) animations!
UIView.animate(withDuration: duration, delay: 0.0, options: [], animations: {
fromView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
}, completion: {_ in
//3) complete the transition
transitionContext.completeTransition(
!transitionContext.transitionWasCancelled
)
})
}
}
The Image Revealed
Now for the final step, the view controller that reveals the image. In your ImageRevealController add this:
import UIKit
class ImageRevealViewController: UIViewController {
var imageToReveal: UIImage!
#IBOutlet weak var imageRevealed: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
imageRevealed.image = imageToReveal
}
#IBAction func backButton(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
}
The backButton connects to the button that I added to the view that acts like nav bar. You can add your own back indicator to make it more authentic.
For more info on UIViewControllerTransitioningDelegate there is a section here "From View Controller" disappears using UIViewControllerContextTransitioning, you could look into and to which I have contributed an answer.
To create a Photo Gallery, you can refer to : https://github.com/inspace-io/INSPhotoGallery.
It is a nice library for showing photos, zooming funcationality and many more.
I have a Table View with basic label of Table View Cell. I labelled the cell as "January", "February", "March", etc. When user tap on "January", an image with file name "jan.jpeg" will be showed using the following Swift code:
override func viewDidLoad() {
super.viewDidLoad()
let image = UIImage(named: "jan.jpeg")!
imageView = UIImageView(image: image)
imageView.frame = CGRect(origin: CGPointMake(0.0, 0.0), size:image.size)
}
My question is, is it possible when user tap on "February", "feb.jpeg" will be showed, whereas "mar.jpeg will be showed if user tap on "Mar"? How to implement this?
When a cell is selected, the tableView:didSelectRowAtIndexPath: delegate method is called. If you have a predefined list of cells, you can test which cell is being tapped and load the correct image. For example:
have a variable var imgName: String outside of all functions in the viewcontroller with your tableView.
Also put this function in the viewcontroller with your tableView:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.row == 0 {
imgName = "jan.jpeg"
}
else if indexPath.row == 1 {
imgName = "feb.jpeg"
}
else if indexPath.row == 2 {
imgName = "mar.jpeg"
}
else {
// Handle else
}
self.performSegueWithIdentifier("showImageSegue", sender: self)
}
You will need to go to interface builder and name the segue between your viewcontrollers to "showImageSegue".
Additionally, implement the prepareForSegue: function:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
let destinationVC = segue.destinationViewController as ImageViewController // Replace ImageViewController with whatever the name of your destination viewcontroller is.
destinationVC.imageName = imgName
}
Finally, inside the ImageViewController class, add this:
var imageName: String
override func viewDidLoad() {
let image = UIImage(named: imageName)
imageView = UIImageView(image: image)
imageView.frame = CGRect(origin: CGPointMake(0,0, 0,0), size: image.size)
}
Please bear with me. I'm new to programming and new to StackOverflow. I hope that my question will grant me a warm response for entering the programming community. An acquaintance, whose opinion I trust, told me to post this email to him on StackOverflow.
What do I have to do to get two instances of NSObject to work in the same ViewController? I've initialized an NSObject subclass called SideBar and RightSideBar. They both draw from NSObject. The cells in the menu are called created by a TableViewController I created programatically. I followed a tutorial that did everything programmatically because I didn't know that Storyboard is better for building things.
Below is the code base.
EDITED: Sorry for being long winded. I don't know how to make this any shorter and as complete
===========
****Note the SideBar subclass is the left menu. The RightSideBar class has the same initializer setup and is the right menu. I want to be able to make them both appear on the same ViewController in the same instance of the same ViewController if possible.****
This is the left TableViewController:
import UIKit
//this protocol of the sidebar delegate manages the selection of the item in the row.
protocol SidebarTableViewControllerDelegate {
func sidebarControlDidSelectRow(indexPath: NSIndexPath)
}
class SidebarTableViewController: UITableViewController {
//setting up the delegate and array of menu items.
var delegate:SidebarTableViewControllerDelegate?
var tableData:Array <String> = []
var imageData:[UIImage] = []
// MARK: - Table view data source
//Setting up the number of sections in the menu
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
//Setting up the number of items in the menu
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
//Setting up the menu look for the main screen after login.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("Cell") as? UITableViewCell
if cell == nil {
cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
//configure the cell...
cell!.backgroundColor = UIColor.clearColor()
cell!.textLabel?.textColor = UIColor.darkTextColor()
let selectedView:UIView = UIView(frame: CGRect(x: 0, y: 0, width: cell!.frame.size.width, height: cell!.frame.size.height))
selectedView.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.3)
cell!.selectedBackgroundView = selectedView
}
cell!.textLabel!.text = tableData[indexPath.row]
cell!.imageView!.image = imageData[indexPath.row]
return cell!
}
//Setting up the height for each cell of the table
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 45.0
}
//Setting up the selection of the item in the cell.
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
delegate?.sidebarControlDidSelectRow(indexPath)
}
override func viewDidLoad() {
}
override func didReceiveMemoryWarning() {
}
}
This is the right table view controller:
//setting up the RightSidebarControllerDelegate
protocol RightSidebarTableViewControllerDelegate {
func rightSidebarControlDidSelectRow(indexPath: NSIndexPath)
}
class RightSidebarTableViewController: UITableViewController {
//setting up the delegate and array of menu items.
var delegate:RightSidebarTableViewControllerDelegate?
var rightTableData:Array <String> = []
var rightImageData:[UIImage] = []
// MARK: - Table view data source
//Setting up the number of sections in the menu
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
//Setting up the number of items in the menu
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return rightTableData.count
}
//Setting up the menu look for the main screen after login.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("Cell") as? UITableViewCell
if cell == nil {
cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
//configure the cell...
cell!.backgroundColor = UIColor.clearColor()
cell!.textLabel?.textColor = UIColor.darkTextColor()
let selectedView:UIView = UIView(frame: CGRect(x: 0, y: 0, width: cell!.frame.size.width, height: cell!.frame.size.height))
selectedView.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.3)
cell!.selectedBackgroundView = selectedView
}
cell!.textLabel!.text = rightTableData[indexPath.row]
cell!.imageView!.image = rightImageData[indexPath.row]
return cell!
}
//Setting up the height for each cell of the table
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 45.0
}
//Setting up the selection of the item in the cell.
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
delegate?.rightSidebarControlDidSelectRow(indexPath)
}
override func viewDidLoad() {
}
override func didReceiveMemoryWarning() {
}
}
Here is where my problems may start with SideBar:NSObject. This is the left SideBar to be initialized:
import UIKit
#objc protocol SideBarDelegate {
func sideBarDidSelectButtonAtIndex (index: Int)
optional func sideBarWillClose()
optional func sideBarWillOpen()
optional func sideBarWillDeinitialize()
}
//this class sets up the actual sidebar.
class SideBar: NSObject, SidebarTableViewControllerDelegate {
//width of the bar, tableview setup, and views for the sidebar
let barWidth:CGFloat = 175.0
let sideBarTableViewTopInset:CGFloat = 25.0
let sideBarContainerView:UIView = UIView()
let sideBarTableViewController:SidebarTableViewController = SidebarTableViewController()
var originView:UIView!
//var for dynamic effect and controlling the sidebar
var animator:UIDynamicAnimator!
var delegate:SideBarDelegate?
var isSideBarOpen:Bool = false
//initializer for the "SideBar" class.
override init() {
super.init()
}
//initializer for the tableView of menu items.
init(sourceView: UIView, menuItems: Array<String>, menuImages: [UIImage]){
super.init()
//initializing the views and animation for the menu.
originView = sourceView
sideBarTableViewController.tableData = menuItems
sideBarTableViewController.imageData = menuImages
setupSideBar()
animator = UIDynamicAnimator(referenceView: originView)
}
//function for setting up the sidebar.
func setupSideBar () {
//setting up the frame/outline of the side bar.
sideBarContainerView.frame = CGRectMake(-barWidth, originView.frame.origin.y + 45, barWidth, originView.frame.size.height)
//setting up the color of the sidebar.
sideBarContainerView.backgroundColor = UIColor.clearColor()
//disables subviews from being confined to the sidebar.
sideBarContainerView.clipsToBounds = false
//placing the sidebar in the UIView
originView.addSubview(sideBarContainerView)
//adding blur to the menu.
let blurView:UIVisualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffectStyle.Light))
blurView.frame = sideBarContainerView.bounds
sideBarContainerView.addSubview(blurView)
//setting up controls for the sidebar
sideBarTableViewController.delegate = self
sideBarTableViewController.tableView.frame = sideBarContainerView.bounds
sideBarTableViewController.tableView.clipsToBounds = false
//disabling the scroll feature. Delete to keep the scroll feature.
sideBarTableViewController.tableView.scrollsToTop = false
//This will remove separators in the UITableCell. Delete to keep separators.
sideBarTableViewController.tableView.separatorStyle = UITableViewCellSeparatorStyle.None
//This sets the background color of the sidebar and creates the inset.
sideBarTableViewController.tableView.backgroundColor = UIColor.clearColor()
sideBarTableViewController.tableView.contentInset = UIEdgeInsets(top: sideBarTableViewTopInset, left: 0, bottom: 0, right: 0)
//reloads the sidebar and adds the container view to the sideBarTableViewController.
sideBarTableViewController.tableView.reloadData()
sideBarContainerView.addSubview(sideBarTableViewController.tableView)
}
func showSideBar(shouldOpen: Bool){
animator.removeAllBehaviors()
isSideBarOpen = shouldOpen
//simple if and else statements to define the direction of animation and intensity of animation
let gravityX:CGFloat = (shouldOpen) ? 0.5 : -0.5
let magnitude:CGFloat = (shouldOpen) ? 20 : -20
let boundaryX:CGFloat = (shouldOpen) ? barWidth : -barWidth
//controls the behavior of the animation.
let gravityBehavior: UIGravityBehavior = UIGravityBehavior(items: [sideBarContainerView])
gravityBehavior.gravityDirection = CGVectorMake(gravityX, 0)
animator.addBehavior(gravityBehavior)
let collisionBehavior: UICollisionBehavior = UICollisionBehavior(items: [sideBarContainerView])
collisionBehavior.addBoundaryWithIdentifier("sideBarBoundary", fromPoint: CGPointMake(boundaryX, 20), toPoint: CGPointMake(boundaryX, originView.frame.size.height))
animator.addBehavior(collisionBehavior)
let pushBehavior:UIPushBehavior = UIPushBehavior(items: [sideBarContainerView], mode: UIPushBehaviorMode.Instantaneous)
pushBehavior.magnitude = magnitude
animator.addBehavior(pushBehavior)
let sideBarBehavior:UIDynamicItemBehavior = UIDynamicItemBehavior(items: [sideBarContainerView])
sideBarBehavior.elasticity = 0.3
animator.addBehavior(sideBarBehavior)
}
func sidebarControlDidSelectRow(indexPath: NSIndexPath) {
delegate?.sideBarDidSelectButtonAtIndex(indexPath.row)
}
}
This is the right SideBar:NSObject that will eventually initialize the right menu.
import UIKit
#objc protocol RightSideBarDelegate {
func rightSideBarDidSelectButtonAtIndex (index: Int)
optional func sideBarWillClose()
optional func sideBarWillOpen()
}
class RightSideBar: NSObject, RightSidebarTableViewControllerDelegate {
//width of the bar, tableview setup, and views for the sidebar
let barWidth:CGFloat = 175.0
let rightSideBarTableViewTopInset:CGFloat = 25.0
let rightSideBarContainerView:UIView = UIView()
let rightSideBarTableViewController:RightSidebarTableViewController = RightSidebarTableViewController()
var rightOriginView:UIView!
//var for dynamic effect and controlling the sidebar
var animator:UIDynamicAnimator!
var delegate:RightSideBarDelegate?
var isSideBarOpen:Bool = false
//initializer for the "SideBar" class.
override init() {
super.init()
}
//initializer for the tableView of menu items.
init(rightSourceView: UIView, rightMenuItems: Array<String>, rightMenuImages: [UIImage]){
super.init()
//initializing the views and animation for the menu.
rightOriginView = rightSourceView
rightSideBarTableViewController.rightTableData = rightMenuItems
rightSideBarTableViewController.rightImageData = rightMenuImages
setupSideBar()
animator = UIDynamicAnimator(referenceView: rightOriginView)
}
//function for setting up the sidebar.
func setupSideBar () {
//setting up the frame/outline of the side bar.
rightSideBarContainerView.frame = CGRectMake(rightOriginView.frame.size.width + barWidth , rightOriginView.frame.origin.y + 45, barWidth, rightOriginView.frame.size.height)
//setting up the color of the sidebar.
rightSideBarContainerView.backgroundColor = UIColor.clearColor()
//disables subviews from being confined to the sidebar.
rightSideBarContainerView.clipsToBounds = false
//placing the sidebar in the UIView
rightOriginView.addSubview(rightSideBarContainerView)
//adding blur to the menu.
let blurView:UIVisualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffectStyle.Light))
blurView.frame = rightSideBarContainerView.bounds
rightSideBarContainerView.addSubview(blurView)
//setting up controls for the sidebar
rightSideBarTableViewController.delegate = self
rightSideBarTableViewController.tableView.frame = rightSideBarContainerView.bounds
rightSideBarTableViewController.tableView.clipsToBounds = false
//disabling the scroll feature. Delete to keep the scroll feature.
rightSideBarTableViewController.tableView.scrollsToTop = false
//This will remove separators in the UITableCell. Delete to keep separators.
rightSideBarTableViewController.tableView.separatorStyle = UITableViewCellSeparatorStyle.None
//This sets the background color of the sidebar and creates the inset.
rightSideBarTableViewController.tableView.backgroundColor = UIColor.clearColor()
rightSideBarTableViewController.tableView.contentInset = UIEdgeInsets(top: rightSideBarTableViewTopInset, left: 0, bottom: 0, right: 0)
//reloads the sidebar and adds the container view to the rightSideBarTableViewController.
rightSideBarTableViewController.tableView.reloadData()
rightSideBarContainerView.addSubview(rightSideBarTableViewController.tableView)
}
func showSideBar(shouldOpen: Bool){
animator.removeAllBehaviors()
isSideBarOpen = shouldOpen
//simple if and else statements to define the direction of animation and intensity of animation
let gravityX:CGFloat = (shouldOpen) ? -0.5 : 0.5
let magnitude:CGFloat = (shouldOpen) ? -20 : 20
let boundaryX:CGFloat = (shouldOpen) ? -barWidth : barWidth
//controls the behavior of the animation.
let gravityBehavior: UIGravityBehavior = UIGravityBehavior(items: [rightSideBarContainerView])
gravityBehavior.gravityDirection = CGVectorMake(gravityX, 0)
animator.addBehavior(gravityBehavior)
let collisionBehavior: UICollisionBehavior = UICollisionBehavior(items: [rightSideBarContainerView])
collisionBehavior.addBoundaryWithIdentifier("sideBarBoundary", fromPoint: CGPointMake(boundaryX, 20), toPoint: CGPointMake(boundaryX, rightOriginView.frame.size.height))
animator.addBehavior(collisionBehavior)
let pushBehavior:UIPushBehavior = UIPushBehavior(items: [rightSideBarContainerView], mode: UIPushBehaviorMode.Instantaneous)
pushBehavior.magnitude = magnitude
animator.addBehavior(pushBehavior)
let sideBarBehavior:UIDynamicItemBehavior = UIDynamicItemBehavior(items: [rightSideBarContainerView])
sideBarBehavior.elasticity = 0.3
animator.addBehavior(sideBarBehavior)
}
func rightSidebarControlDidSelectRow(indexPath: NSIndexPath) {
delegate?.rightSideBarDidSelectButtonAtIndex(indexPath.row)
}
}
Finally this is my current code for the DoubleMenuViewController. Something happens when I segue to the DoubleMenuViewController to break the menus. The menus won't even load. However, if I'm in a SingleMenuViewController that only calls SideBar:NSObject then the code will work so long as I'm only calling one menu. In this DoubleMenuViewController, I have the initialization section commented out for the RightSideBar class because I'm working on a solution. I know this code for this ViewController is garbled. I'm trying everything I can think of. See my remarks after the code to see what I've tried:
import UIKit
class DoubleMenuViewController: UIViewController, SideBarDelegate, RightSideBarDelegate {
var sideBar:SideBar?
var ondemandSideBar:SideBar {
get {
if sideBar == nil {
//setting up the menu items for the sidebar.
sideBar = SideBar(sourceView: self.view, menuItems: ["Home", "Share", "About", "Help"], menuImages: [homeImage!, shareImage!, aboutImage!, helpImage!])
sideBar!.delegate = self
SideBar.new()
}
return sideBar!
}
}
//initializes the "RightSideBar"
var rightSideBar:RightSideBar?
var ondemandRightSideBar:RightSideBar {
get {
if rightSideBar == nil {
rightSideBar = RightSideBar(rightSourceView: self.view, rightMenuItems: [//Other items], rightMenuImages: [//Other Items])
rightSideBar!.delegate = self
RightSideBar.new()
}
return rightSideBar!
}
}
var homeImage = UIImage(named: "Home")
var shareImage = UIImage(named: "Share")
var aboutImage = UIImage(named: "About")
var helpImage = UIImage(named: "Help")
#IBOutlet weak var currentMenuControl: UIBarButtonItem!
#IBAction func currentMenuDisplay(sender: AnyObject) {
if currentMenuControl.tag == 1 {
ondemandSideBar.showSideBar(true)
currentMenuControl.tag = 0
} else {
ondemandSideBar.showSideBar(false)
currentMenuControl.tag = 1
}
}
#IBOutlet weak var progressionMenuControl: UIBarButtonItem!
#IBAction func progressionMenuDisplay(sender: AnyObject) {
if progressionMenuControl.tag == 1 {
ondemandRightSideBar.showSideBar(true)
progressionMenuControl.tag = 0
} else {
ondemandRightSideBar.showSideBar(false)
progressionMenuControl.tag = 1
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func sideBarDidSelectButtonAtIndex(index: Int) {
switch index {
//segues
}
}
func rightSideBarDidSelectButtonAtIndex(index: Int) {
switch index {
//segues
}
}
}
Here's what I've tried:
I've tried altering the positioning of CGFloats since SubViews seem
to come from the left.
I've renamed all the RightSideBar variables and class names
everything to overcome a runtime confusion in instance variables and
class names. This includes renaming the initializers that you saw in
the NSObject SubClass and the target view controller.
I've tried using control flow in the viewDidLoad method with a
button tag. I took away the swipe features to show the menu and
added buttons because I thought system was struggling to deal with
the Swipes.
I've tried deinitializing in the SideBar subclass file of NSObject.
All that got me was an infinite loop that crashed the application
after login.
Then I tried ondemand initialization in the
targetViewController.....DoubleMenuViewController and
SingleMenuViewController. I returned to a working menu with buttons
in the SingleMenuViewController but it still won't show the left and
right menu in the DoubleMenuViewController.
Last I tried deinitializing the SideBar (left SideBar) and the RightSideBar in the DoubleMenuViewController. However, when I add println() functions to all my sections the debugger doesn't run the print function for me to get values of objects or even show typed states like "This". I added the print functions because I wasn't sure if I would know when deinitialization and reinitialization occurred.
It seems that my menu is initialized from the SideBar: NSObject file and the RightSideBar:NSObject file. What I mean is that my menu is being created before I hit the target view controller. This isn't a problem for me so long as I can get the compiler to initialize the SideBar and the RightSideBar in the same View Controller, but it won't do that.
I just need to be able to control both menus with swipes or button taps.
I think I have a problem with my initializers overriding each other.
However, I don't know how to fix that problem. I've read through the Swift manual and read articles on the internet. I've also searched StackOverflow.
You ask:
How do I Initialize two instances of NSObject in the same view controller?
Setting aside why you're dealing with NSObject at all (in Objective-C all classes have to subclass from NSObject ultimately, in Swift that's no longer the case), if you want to instantiate two objects, you simply have to have one property for each.
If these are lazily instantiated, as your code snippet suggests, then you have to identify where that lazily instantiated property is referenced (e.g. you might trigger it from a "swipe from edge" gesture) or what have you. Set a breakpoint in the code that references that lazily instantiated property and make sure you're getting there at all.
--
I had some observations on one of your code snippets. You say that you're instantiating your side bar like so:
var sideBar : SideBar?
var ondemandSideBar : SideBar {
get {
if sideBar == nil {
sideBar = SideBar(sourceView, menuItems, menuImage etc.)
sideBar!.delegate
SideBar.new()
}
}
}
I don't think that's what you're really doing because you're not setting the delegate, you're both instantiating the SideBar as well as calling new (which you shouldn't be doing from Swift), you're not returning a value, etc.
Also, that pattern of having a stored property that is instantiated by some computed property has a certain Objective-C je ne sais quoi. I'm inferring that you want a lazily instantiated property. If that's the case, I'd be inclined to use a single lazy stored property. And I'd then set that property lazily using a closure:
I'd expect something like this pattern
protocol SideBarDelegate : class { // specify class so we can use `weak` reference
func didChooseMenuItem(sender: SideBar, item: Int)
}
class SideBar {
weak var delegate: SideBarDelegate? // make sure this is weak to avoid strong reference cycle
}
class ViewController: UIViewController, SideBarDelegate {
lazy var sideBar: SideBar = {
let _sideBar = SideBar(sourceView, menuItems, menuImage, etc.)
_sideBar.delegate = self
return _sideBar
}()
func didChooseMenuItem(sender: SideBar, item: Int) {
// handle it here
}
// etc.
}
This way, the sideBar won't be instantiated until you reference sideBar somewhere in your code, but when you do, it will be instantiated with the code inside that closure.
I have a Swift project, learning a weather API and also trying to get a better handle on the AnimatedTransitions. I have a UITableView using a custom UITableViewCell with images and text. Tapping a cell in the tableView transitions to a new UIViewController as a Show (push), with the whole thing embedded in a UINavigationController.
When the transition is invoked, the image from the cell is supposed to move to the final location of the UIImageView on the destination viewController. However, what it does is move past that point to the far side of the screen before the transition completes and the view changes, making the image appear to snap back to the center of the view.
I have read a lot of tutorials trying to fix this and have read a lot of StackOverflow but have failed to figure it out. Can someone point out to me what I have missed, please? I'm going crazy, here.
The segue that invokes the transition, in the original ViewController:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.performSegueWithIdentifier("SHOW_DETAIL", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "SHOW_DETAIL" {
let detailVC = segue.destinationViewController as DetailViewController
let indexPathForForecast = self.tableView.indexPathForSelectedRow() as NSIndexPath!
let detailForecast = self.forecasts?[indexPathForForecast.row]
let cell = self.tableView.cellForRowAtIndexPath(indexPathForForecast) as WeatherCell
let image = cell.forecastImage.image
detailVC.forecastForDetail = detailForecast
detailVC.forecastDetailImage = image
}
}
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if fromVC == self && toVC.isKindOfClass(DetailViewController) {
let transitionVC = AnimateToDetailVCController()
return transitionVC
} else {
return nil
}
}
And here's the animateTransition code from the UIViewControllerAnimatedTransitioning object (EDIT: solution code edited into code block, thanks #jrturton!)
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as ViewController
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as DetailViewController
let containerView = transitionContext.containerView()
let duration = self.transitionDuration(transitionContext)
let selectedRow = fromViewController.tableView.indexPathForSelectedRow()
let cell = fromViewController.tableView.cellForRowAtIndexPath(selectedRow!) as WeatherCell
let weatherSnapshot = cell.forecastImage.snapshotViewAfterScreenUpdates(false)
weatherSnapshot.frame = containerView.convertRect(cell.forecastImage.frame, fromView: fromViewController.tableView.cellForRowAtIndexPath(selectedRow!)?.superview)
cell.forecastImage.hidden = true
toViewController.view.frame = transitionContext.finalFrameForViewController(toViewController)
toViewController.view.alpha = 0
toViewController.detailImageView.hidden = true
containerView.addSubview(toViewController.view)
containerView.addSubview(weatherSnapshot)
var toFrame = toViewController.locationIs
UIView.animateWithDuration(duration, animations: { () -> Void in
// EDIT: This solved the issue, thanks JRTurton!
toViewController.view.setNeedsLayout() // Solution: This is where it was needed
toViewController.view.layoutIfNeeded() // Solution: This is where it was needed
toViewController.view.alpha = 1.0
var endRect = containerView.convertRect(toViewController.detailImageView.frame, fromView: toViewController.view)
weatherSnapshot.frame = endRect
}) { (finished) -> Void in
toViewController.detailImageView.hidden = false
cell.forecastImage.hidden = false
weatherSnapshot.removeFromSuperview()
transitionContext.completeTransition(true)
}
}
It's hard to say, but here's my guess: you're using size classes, designing at the any/any size, and the image in your to view controller is still centered in respect of that when you get its frame to use for your animation, making it too far to the right. Once the transition is complete, a layout pass happens and it gets corrected.
To fix, after you set the frame of the to view controller, force a layout pass:
toViewController.view.setNeedsLayout()
toViewController.view.layoutIfNeeded()
Before making the above change, you can first confirm if this is the issue by checking the image view's frame before the animation.