UIPageViewController transition bugged if animation starts in the meanwhile - ios

I have a strange behaviour in my app using a UIPageViewController.
The layout of my app is a PageViewController (camera roll like) with a ads banner on bottom.
The banner's container starts as hidden and, when the ad gets loaded, i set the isHidden=false with an animation.
My problem is that when the banner gets into the screen it breaks the UIPageViewController transition if in progress as shown in this video:
I made a new project that reproduces the error very easy with a few lines, you can checkout it in GITHUB: You just need to spam the "Next" button until the banner gets loaded. It also can be reproduced by swipping the PageViewController but is harder to reproduce.
The full example code is:
class TestViewController: UIViewController,UIPageViewControllerDelegate,UIPageViewControllerDataSource {
#IBOutlet weak var constraintAdviewHeight: NSLayoutConstraint!
weak var pageViewController : UIPageViewController?
#IBOutlet weak var containerAdView: UIView!
#IBOutlet weak var adView: UIView!
#IBOutlet weak var containerPager: UIView!
var currentIndex = 0;
var clickEnabled = true
override func viewDidLoad() {
super.viewDidLoad()
let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
pageViewController = pageVC
pageVC.delegate = self
pageVC.dataSource = self
addChildViewController(pageVC)
pageVC.didMove(toParentViewController: self)
containerPager.addSubview(pageVC.view)
pageVC.view.translatesAutoresizingMaskIntoConstraints = true
pageVC.view.frame = containerPager.bounds
pushViewControllerForCurrentIndex()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.containerAdView.isHidden = true
DispatchQueue.main.asyncAfter(deadline: .now()+4) {
self.simulateBannerLoad()
}
}
#IBAction func buttonClicked(_ sender: Any) {
guard clickEnabled else {return}
currentIndex -= 1;
pushViewControllerForCurrentIndex()
}
#IBAction func button2Clicked(_ sender: Any) {
guard clickEnabled else {return}
currentIndex += 1;
pushViewControllerForCurrentIndex()
}
private func simulateBannerLoad(){
constraintAdviewHeight.constant = 50
pageViewController?.view.setNeedsLayout()
UIView.animate(withDuration: 0.3,
delay: 0, options: .allowUserInteraction, animations: {
self.containerAdView.isHidden = false
self.view.layoutIfNeeded()
self.pageViewController?.view.layoutIfNeeded()
})
}
//MARK: data source
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
return getViewControllerForIndex(currentIndex+1)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
return getViewControllerForIndex(currentIndex-1)
}
func getViewControllerForIndex(_ index:Int) -> UIViewController? {
guard (index>=0) else {return nil}
let vc :UIViewController = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(withIdentifier: "pageTest")
vc.view.backgroundColor = (index % 2 == 0) ? .red : .green
return vc
}
func pushViewControllerForCurrentIndex() {
guard let vc = getViewControllerForIndex(currentIndex) else {return}
print("settingViewControllers start")
clickEnabled = false
pageViewController?.setViewControllers([vc], direction: .forward, animated: true, completion: { finished in
print("setViewControllers finished")
self.clickEnabled = true
})
}
}
Note: Another unwanted effect is that the last completion block when the bug occurs does not get called, so it leaves the buttons disabled:
func pushViewControllerForCurrentIndex() {
guard let vc = getViewControllerForIndex(currentIndex) else {return}
print("settingViewControllers start")
clickEnabled = false
pageViewController?.setViewControllers([vc], direction: .forward, animated: true, completion: { finished in
print("setViewControllers finished")
self.clickEnabled = true
})
}
Note2: The banner load event is something I can't control manually. Due to the library used for displaying ads its a callback in the main thread that can happen in any moment. In the sample proyect this is simulated with a DispatchQueue.main.asyncAfter: call
How can I fix that? Thanks

What wrong?
Layout will interrupt animation.
How to prevent?
Not change layout when pageViewController animating.
When the ad is loaded:
Confirm whether pageViewController is animating, if so, wait until the animation is completed and then update, or update
Sample:
private func simulateBannerLoad(){
if clickEnabled {
self.constraintAdviewHeight.constant = 50
} else {
needUpdateConstraint = true
}
}
var needUpdateConstraint = false
var clickEnabled = true {
didSet {
if clickEnabled && needUpdateConstraint {
self.constraintAdviewHeight.constant = 50
needUpdateConstraint = false
}
}
}

For us, it was due to a device rotation causing a layout pass at the same time as an animation to a new view controller. We could not figure out how to stop the layout pass in this situation.
This worked for us:
let pageController = UIPageViewController()
func updatePageController() {
pageController.setViewControllers(newViewControllers, direction: .forward, animated: true, completion: {[weak self] _ in
// There is a bug with UIPageViewController where a layout pass during an animation
// to a new view controller can cause the UIPageViewController to display the old and new view controller
// In our case, we can compare the view controller `children` count against the the `viewControllers` count
// In other cases, we may need to examine `children` more closely against `viewControllers` to see if there discrepancies.
// Since this is on the animation callback we need to dispatch to the main thread to work around another bug: https://stackoverflow.com/a/24749239/2191796
DispatchQueue.main.async { [weak self] in
guard let self = self else {return}
if self.pageController.children.count != self.pageController.viewControllers?.count {
self.pageController.setViewControllers(self.pageController.viewControllers, direction: .forward, animated: false, completion: nil)
}
}
})
}

Related

Child View Controllers in Page View Controller Failing to Receive Delegate Calls

I am having an issue with my two child view controllers inside a parent PageViewController, where a delegate called by one of the children is not received by the other child.
My first child contains buttons, and when a button is pressed, a delegate is triggered in the other child to pause the timer. However, it fails to receive the call and the timer continues to run.
Here is my PageViewController:
class StartMaplessWorkoutPageViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
lazy var workoutViewControllers: [UIViewController] = {
return [self.getNewViewController(viewController: "ButtonsViewController"), self.getNewViewController(viewController: "DisplayMaplessViewController")]
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.dataSource = self
// Saw this from another answer, doesn't do anything that helps (at the moment)
let buttonsViewController = storyboard?.instantiateViewController(withIdentifier: "ButtonsViewController") as! ButtonsViewController
let displayMaplessViewController = storyboard?.instantiateViewController(withIdentifier: "DisplayMaplessViewController") as! DisplayMaplessViewController
buttonsViewController.buttonsDelegate = displayMaplessViewController
if let firstViewController = workoutViewControllers.last {
setViewControllers([firstViewController], direction: .forward, animated: true, completion: nil)
}
let pageControl = UIPageControl.appearance(whenContainedInInstancesOf: [StartWorkoutPageViewController.self])
pageControl.currentPageIndicatorTintColor = .orange
pageControl.pageIndicatorTintColor = .gray
}
func getNewViewController(viewController: String) -> UIViewController {
return (storyboard?.instantiateViewController(withIdentifier: viewController))!
}
// MARK: PageView DataSource
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = workoutViewControllers.firstIndex(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
return workoutViewControllers.last
}
guard workoutViewControllers.count > previousIndex else {
return nil
}
return workoutViewControllers[previousIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = workoutViewControllers.firstIndex(of: viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
let workoutViewControllersCount = workoutViewControllers.count
guard workoutViewControllersCount != nextIndex else {
return workoutViewControllers.first
}
guard workoutViewControllersCount > nextIndex else {
return nil
}
return workoutViewControllers[nextIndex]
}
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return workoutViewControllers.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
guard let firstViewController = viewControllers?.first, let firstViewControllerIndex = workoutViewControllers.firstIndex(of: firstViewController) else {
return 0
}
return firstViewControllerIndex
}
}
My ChildViewController with Buttons:
protocol ButtonsViewDelegate: class {
func onButtonPressed(button: String)
}
class ButtonsViewController: UIViewController {
weak var buttonsDelegate: ButtonsViewDelegate?
var isPaused: Bool = false
#IBOutlet weak var startStopButton: UIButton!
#IBOutlet weak var optionsButton: UIButton!
#IBOutlet weak var endButton: UIButton!
#IBAction func startStopButton(_ sender: Any) {
if isPaused == true {
buttonsDelegate?.onButtonPressed(button: "Start")
isPaused = false
} else {
buttonsDelegate?.onButtonPressed(button: "Pause")
isPaused = true
}
}
#IBAction func endButton(_ sender: Any) {
let menu = UIAlertController(title: "End", message: "Are you sure you want to end?", preferredStyle: .actionSheet)
let end = UIAlertAction(title: "End", style: .default, handler: { handler in
self.buttonsDelegate?.onButtonPressed(button: "End")
})
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
menu.addAction(end)
menu.addAction(cancelAction)
self.present(menu, animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
My other ChildViewController, which should be receiving the calls of the ButtonsViewDelegate:
import UIKit
class DisplayMaplessViewController: UIViewController, ButtonsViewDelegate {
var timer = Timer()
var currentTime: TimeInterval = 0.0
var isCountdown: Bool = false
var isInterval: Bool = false
var currentRepeats: Int = 0
var currentActivity: Int = 0
var count: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
startIntervalTimer(withTime: 0)
}
// Currently not being called
func onButtonPressed(button: String) {
switch button {
case "Start":
restartIntervalTimer()
case "Pause":
pauseIntervalTimer()
case "End":
stop()
default:
break
}
}
func startIntervalTimer(withTime: Double) {
if withTime != 0 {
currentTime = withTime
if isInterval != true {
isCountdown = true
}
}
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(intervalTimerUpdate), userInfo: nil, repeats: true)
}
func pauseIntervalTimer() {
timer.invalidate()
}
func restartIntervalTimer() {
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(intervalTimerUpdate), userInfo: nil, repeats: true)
}
// Currently Not being called
func stop() {
timer.invalidate()
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .positional
formatter.allowedUnits = [.hour, .minute, .second]
formatter.zeroFormattingBehavior = [.pad]
let timeString = formatter.string(from: currentTime)
// save the data etc
print("Stop is called")
}
#objc func intervalTimerUpdate() {
currentTime += 1.0
print(currentTime)
}
}
Sorry that this is so long winded, been trying for quite a while and really annoyed that it doesn't work! Thanks!
I'll try to be clear, hopefully i'll be so as english is not my native language.
It seems to me that you are instantiating your ViewControllers to be presented in the getNewViewController() method and storing them in the workoutViewControllers array, but you are setting the delegate as a separate instance that you never set in your PageVC. You need to set the delegates using the same instances.
These two are two instances of two VC classes (also not sure if the identifier "DisplayViewController" is right, i expected "DisplayMaplessViewController", hard to tell without the storyboard):
let buttonsViewController = storyboard?.instantiateViewController(withIdentifier: "ButtonsViewController") as! ButtonsViewController
let displayMaplessViewController = storyboard?.instantiateViewController(withIdentifier: "DisplayViewController") as! DisplayMaplessViewController
buttonsViewController.buttonsDelegate = displayMaplessViewController
And these in the array two other instances, unrelated from the ones above, of the same two classes:
lazy var workoutViewControllers: [UIViewController] = {
return [self.getNewViewController(viewController: "ButtonsViewController"), self.getNewViewController(viewController: "DisplayMaplessViewController")]
}()
To better understand what i mean, i refactored from scratch and semplified your project (had to do it programmatically as i'm not used to storyboards).
It now consists of a PageController that displays a buttonsVC with a red button and a displayMaplessVC with a blue background.
Once you press the red button, the delegate method is called which causes the blue background to turn green.
Take a look at what i'm doing, as i'm appending the same instances of which i set the delegate:
instantiate a DisplayMaplessViewController object and ButtonsViewController object;
set buttonsVC.buttonsDelegate = displayMaplessVC;
append both ViewControllers to the array.
This is a way to get it done but for sure there are several other ways to achieve the same result, once you get the point and understand your mistake you can pick the one you like the most.
Just copy and paste it into a new project, build and run (you have to set the class of the starting ViewController in the Storyboard as StartMaplessWorkoutPageViewController):
import UIKit
class StartMaplessWorkoutPageViewController: UIViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
private var workoutViewControllers = [UIViewController]()
private let pageController: UIPageViewController = {
let pageController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
return pageController
}()
override func viewDidLoad() {
super.viewDidLoad()
pageController.delegate = self
pageController.dataSource = self
let buttonsVC = ButtonsViewController()
let displayMaplessVC = DisplayMaplessViewController()
buttonsVC.buttonsDelegate = displayMaplessVC
workoutViewControllers.append(buttonsVC)
workoutViewControllers.append(displayMaplessVC)
self.addChild(self.pageController)
self.view.addSubview(self.pageController.view)
self.pageController.setViewControllers([displayMaplessVC], direction: .forward, animated: true, completion: nil)
self.pageController.didMove(toParent: self)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
pageController.view.frame = view.bounds
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = workoutViewControllers.firstIndex(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
return workoutViewControllers.last
}
guard workoutViewControllers.count > previousIndex else {
return nil
}
return workoutViewControllers[previousIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = workoutViewControllers.firstIndex(of: viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
let workoutViewControllersCount = workoutViewControllers.count
guard workoutViewControllersCount != nextIndex else {
return workoutViewControllers.first
}
guard workoutViewControllersCount > nextIndex else {
return nil
}
return workoutViewControllers[nextIndex]
}
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return workoutViewControllers.count
}
}
.
protocol ButtonsViewDelegate: class {
func onButtonPressed()
}
import UIKit
class ButtonsViewController: UIViewController {
weak var buttonsDelegate: ButtonsViewDelegate?
let button: UIButton = {
let button = UIButton()
button.backgroundColor = .red
button.addTarget(self, action: #selector(onButtonPressed), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(button)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
button.frame = CGRect(x: 50,
y: 50,
width: 100,
height: 100)
}
#objc private func onButtonPressed() {
buttonsDelegate?.onButtonPressed()
}
}
.
import UIKit
class DisplayMaplessViewController: UIViewController, ButtonsViewDelegate {
private let testView: UIView = {
let view = UIView()
view.backgroundColor = .blue
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(testView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
testView.frame = view.bounds
}
internal func onButtonPressed() {
testView.backgroundColor = .green
}
}

iOS UIPageViewController Gets Confused and ViewControllers Are Not Displayed

We have left and right buttons set up for the user to page through different cars quickly. Our Page View Controller loses the view controller if the user taps quickly to the next page 10 or more times.
Here is the vehicle page with the car showing correctly (blurred to hide non-relevant information). See image here:
If scrolling animation is on (true), it loses the vehicle page after tapping the right arrow 6 or more times quickly. See image here:
Code:
private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {
let viewControllers = [viewController]
let isAnimated = true // false always works. However, animation is required.
setViewControllers(viewControllers, direction: direction, animated: isAnimated, completion: nil)
}
While debugging and when the page view controller has stopped showing the cars, I ensured that the view controller being set is not nil and the listing (car) is also non-nil.
I tried a variant of the solution from UIPageViewController, how do I correctly jump to a specific page without messing up the order specified by the data source? where the completion block is used. However, it did not work.
weak var pvcw: UIPageViewController? = self
setViewControllers(viewControllers, direction: direction, animated: true, completion: {(_ finished: Bool) -> Void in
let pvcs: UIPageViewController? = pvcw
if pvcs == nil {
return
}
DispatchQueue.main.async(execute: {() -> Void in
pvcs?.setViewControllers(viewControllers, direction: direction, animated: false) {(_ finished: Bool) -> Void in }
})
})
Any ideas? Thank you.
Update
I noticed that sometimes the contained View Controller can be off centered as opposed to entirely missing.
I looked deeper into the scenario of the view controller missing entirely. Clicking on "Debug View Hierarchy" and turning on "Show Clipped Content" revealed the following when the View Controller is missing entirely:
So, it seems the missing content is clipped / out of bounds.
Showing only the wireframes reveals the following:
The Page View Controller has a
_UIPageViewControllerContentView that contains a
_UIQueuingScrollView that contains a
UIView that contains a
VehicleDetailTableViewController (the UITableViewController with a car image and details).
I also see the _UIQueuingScrollView's bounds is quite different when things are weird. The bounds have an x of 1125 as opposed to an X of 375 when everything is normal.
This only happens when using a Transition Style of scroll as opposed to Page Curl. When using Page Curl, things work fine.
How can we prevent / fix this?
Second Update
This code makes the problem go away. However, it leaves a more jarring experience. Perhaps due to the delay of 0.4 seconds, the blue background shows sometimes in normal use.
private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {
let viewControllers = [viewController]
setViewControllers(viewControllers, direction: direction, animated: true, completion: { (_) in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: {
self.setViewControllers(viewControllers, direction: direction, animated: false, completion: nil)
})
})
}
This is not a good user experience. Is there a better approach?
I want the scroll transitions to be smooth, not briefly show the blue background, and to not lose its content aka View Controller content.
Although the real answer is to have View Controllers that are as simple as possible (but no simpler), here is the code that fixed the problem with the side effect of showing the background on occasion when the user navigates to the next View Controller.
private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {
let viewControllers = [viewController]
setViewControllers(viewControllers, direction: direction, animated: true, completion: { (_) in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: {
self.setViewControllers(viewControllers, direction: direction, animated: false, completion: nil)
})
})
}
A simple solution is to decouple the button taps from the view controller changes by adding a small "tap ahead" buffer. Create a button queue (use a simple NSMutableArray that acts as FIFO queue) where you add each navigation button tap, then call a dequeue function if the queue was empty before the add.
In the dequeue function you remove the first entry and change view accordingly, then call itself again in the setViewControllers completion handler if the queue is not empty.
Make sure to do the processing on the main thread only to avoid threading problems. If you want, you can also add restrictions on how many "tap ahead" you allow, and perhaps flush the queue on directional changes.
I'm not sure if this solution would be suitable for your users but if the problem occurs due to the user navigating quickly you could implement a lock that would disallow this quick navigation. Essentially:
private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {
guard !isChangingPages else { return }
isChangingPages = true
let viewControllers = [viewController]
let isAnimated = true // false always works. However, animation is required.
setViewControllers(viewControllers, direction: direction, animated: isAnimated, completion: { [weak self] _ in
self?.isChangingPages = false
})
}
This way you'd have to finish transitioning to the new page before allowing the transition to the next.
This would likely result in confusion for the user if you kept the navigation buttons enabled while this bool was set to true (tapping without seeing a result). But the logic could be changed to disable the buttons and reenable them in the completion block (that way they'd fade in/out during the page change).
Hi I have created a Sample Project, which should solve your problem. I have added 100 ViewControllers (via loop) and it's working fine with scroll animation. things are at their place.
What I have done in this project is:
Created a BaseClass for a Page with two properties
a) pageIndex of type Int
b) delegate for a protocol for callbacks
Added UIPageViewController to ViewController via ContainerView
Created a ViewController named Page which extends PageViewBase
Run a loop count of 100 and added data to an array and set datasource and delegate to self (PageControlelr) and managed it according to pageIndex property
PageViewController
class PageViewController: UIPageViewController {
var list = [Page]()
var sb: UIStoryboard?
var viewController: ViewController! // settting from ViewController
override func viewDidLoad() {
super.viewDidLoad()
sb = UIStoryboard(name: "Main", bundle: nil)
DispatchQueue.main.asyncAfter(deadline: .now()+0.4, execute: {
self.setupList()
})
}
func setupList(){
for i in 0..<100{
let model = PageModel(title: "Title \(i + 1)", subTitle: "SubTitle \(i + 1)")
let page = sb?.instantiateViewController(withIdentifier: "PageID") as! Page
page.data = model
page.pageIndex = i
page.delegate = viewController
list.append(page)
}
self.delegate = self
self.dataSource = self
setViewControllers([list[0]], direction: .forward, animated: true, completion: nil)
self.updateCurrentPageLabel(index: 0)
}
func movePage(index: Int){
let currentIndex = self.viewControllers![0] as! Page
self.updateCurrentPageLabel(index: index)
setViewControllers([list[index]], direction: index > currentIndex.pageIndex ? .forward : .reverse, animated: true)
}
func getCurrentPageIndex() -> Int{
return (self.viewControllers![0] as! Page).pageIndex
}
func updateCurrentPageLabel(index: Int){
(self.parent as? ViewController)?.currentListingLabel.text = "\(index + 1) of \(list.count)"
}
}
extension PageViewController: UIPageViewControllerDelegate{
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
let currentIndex = (self.viewControllers![0] as! Page).pageIndex
self.updateCurrentPageLabel(index: currentIndex)
}
}
extension PageViewController: UIPageViewControllerDataSource{
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
let index = (viewController as! Page).pageIndex
if index > 0 {
return list[index-1]
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
let index = (viewController as! Page).pageIndex
if index < list.count-1 {
return list[index+1]
}
return nil
}
}
Page
import UIKit
struct PageModel {
var title: String
var subTitle: String
}
class Page: PageViewBase {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var subTitleLabel: UILabel!
#IBOutlet weak var imageView: UIImageView!
#IBOutlet weak var btnWorking: UIButton!
var data: PageModel?
override func viewDidLoad() {
super.viewDidLoad()
setupTags()
setupActions()
setupData()
}
func setupData(){
if let data = data{
self.titleLabel.text = data.title
self.subTitleLabel.text = data.subTitle
imageView.image = #imageLiteral(resourceName: "car")
}
}
enum buttonTags: Int{
case working = 1
}
func setupTags(){
btnWorking.tag = buttonTags.working.rawValue
}
func setupActions(){
btnWorking.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
}
#objc func didSelect(_ sender: UIView){
if let tag = buttonTags.init(rawValue: sender.tag){
switch tag{
case .working:
delegate?.didReceive(withMessage: "wokring button clicked of index \(pageIndex)")
}
}
}
}
ViewController // MainController
import UIKit
protocol CallBack {
func didReceive(withMessage message: String)
}
class ViewController: UIViewController {
#IBOutlet weak var containerView: UIView!
#IBOutlet weak var btnCall: UIButton!
#IBOutlet weak var btnMessage: UIButton!
#IBOutlet weak var btnNext: UIButton!
#IBOutlet weak var btnBack: UIButton!
#IBOutlet weak var currentListingLabel: UILabel!
var pageController: PageViewController?
override func viewDidLoad() {
super.viewDidLoad()
setupTags()
setupActions()
setupContainerView()
}
enum buttonTags: Int{
case call = 1
case message
case next
case back
}
func setupTags(){
btnCall.tag = buttonTags.call.rawValue
btnMessage.tag = buttonTags.message.rawValue
btnNext.tag = buttonTags.next.rawValue
btnBack.tag = buttonTags.back.rawValue
}
func setupActions(){
btnCall.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
btnMessage.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
btnNext.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
btnBack.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
}
#objc func didSelect(_ sender: UIView){
if let tag = buttonTags.init(rawValue: sender.tag){
switch tag{
case .call:
print("Call button called for index \(pageController?.getCurrentPageIndex() ?? 0)")
case .message:
print("message button called for index \(pageController?.getCurrentPageIndex() ?? 0)")
case .next:
if let p = pageController{
let currentIndex = p.getCurrentPageIndex()
if currentIndex < p.list.count - 1{
p.movePage(index: currentIndex + 1)
}
}
case .back:
if let p = pageController{
let currentIndex = p.getCurrentPageIndex()
if currentIndex > 0{
p.movePage(index: currentIndex - 1)
}
}
}
}
}
func setupContainerView(){
let sb = UIStoryboard(name: "Main", bundle: nil)
pageController = sb.instantiateViewController(withIdentifier: "PageViewControllerID") as? PageViewController
pageController?.viewController = self
addViewIntoParentViewController(vc: pageController)
}
func addViewIntoParentViewController(vc: UIViewController?){
if let vc = vc{
for v in self.containerView.subviews{
v.removeFromSuperview()
}
self.containerView.addSubview(vc.view)
self.containerView.translatesAutoresizingMaskIntoConstraints = false
vc.view.translatesAutoresizingMaskIntoConstraints = false
addChildViewController(vc)
NSLayoutConstraint.activate([
vc.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
vc.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
vc.view.topAnchor.constraint(equalTo: containerView.topAnchor),
vc.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
])
vc.didMove(toParentViewController: self)
}
}
}
extension ViewController: CallBack{
func didReceive(withMessage message: String) {
print("message: \(message)")
}
}
PageViewBase
import UIKit
class PageViewBase: UIViewController {
var pageIndex = -1
var delegate: CallBack?
}

Assertion failure in UIPageViewController

I have an Assertion Failure in UIPageViewController.
Assertion failure in -[UIPageViewController _flushViewController:animated:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit/UIKit-3512.60.12/UIPageViewController.m
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason:
'Don't know about flushed view <UIView: 0x15a5bff30; frame = (0 0; 768 903); autoresize = W+H; layer = <CALayer: 0x15a5bfc30>>'
*** First throw call stack:
(0x181ebedb0 0x181523f80 0x181ebec80 0x182844154 0x1877a1c40 0x1877a1da8 0x18784e9c4 0x18784ebfc 0x187852318 0x18784dd98 0x1870101e4 0x1849a2994 0x18499d5d0 0x1870270a4 0x10028b620 0x100348b78 0x100379f54 0x100168878 0x18733d568 0x1870330b4 0x1870f1a00 0x18733e71c 0x1870f832c 0x18703536c 0x18700f7ac 0x18700ed40 0x18700eba8 0x1873283b4 0x18700d5e8 0x18784ebd4 0x187852318 0x18784df3c 0x1871db550 0x1871daf6c 0x101c9b768 0x1849f0234 0x1849f00e8 0x182135e54 0x181e5d030 0x181e757d4 0x181e74f0c 0x181e72c64 0x181d9cc50 0x183684088 0x18707e088 0x10033b200 0x18193a8b8)
libc++abi.dylib: terminating with uncaught exception of type NSException
I don't know why this error is occurring. Any clues on what's causing it or how to debug it?
The direct way to run into this assert is to use cycled source for UIPageController defined with scroll transition style.
When the source contains two pages each one is the previous and the next for another one. If you swipe UIPageController containing two pages and then try to set source with 3 pages you will get the assertion mentioned above with guarantee assuming that UIPageControllerDataSource before/after methods allow cycled transition in case of 2 pages.
The main rules of crash-free using UIPageController with scroll transition:
1) set dataSource before calling setViewControllers method
2) use setViewControllers method without animation (animated: false)
3) set dataSource to nil for single page mode
4) don't allow cycles for 2-page mode
All these recommendations together make UIPageController absolutely stable.
import UIKit
/// Convenient subclass of UIPageViewController
#objc class AMPageViewController: UIPageViewController {
/// Turn on/off PageControl at the bottom
#objc var showPageControl: Bool = true
/// Array of all viewControllers
#objc var source: [UIViewController]? {
didSet {
let count = source?.count ?? 0
if count > 0 {
dataSource = count > 1 ? self : nil
}
else {
dataSource = nil
delegate = nil
}
}
}
/// Index of the current viewController from source
#objc var pageIndex: Int {
get {
var currentPageIndex: Int = 0
if let vc = viewControllers?.first, let source = source, let pageIndex = source.index(of: vc) {
currentPageIndex = pageIndex
}
return currentPageIndex
}
set {
guard newValue >= 0, let source = source, newValue < source.count else { return }
let vc = source[newValue]
let direction: UIPageViewControllerNavigationDirection = newValue < pageIndex ? .reverse : .forward
setViewController(vc, direction: direction)
}
}
override weak var delegate: UIPageViewControllerDelegate? {
get { return super.delegate }
set {
if source?.count ?? 0 > 0 {
super.delegate = newValue
}
else {
super.delegate = nil
}
}
}
/// Initializer in scroll-mode with interPageSpacing
#objc init(navigationOrientation: UIPageViewControllerNavigationOrientation = .horizontal, interPageSpacing: Int = 0) {
let options = (interPageSpacing > 0) ? [UIPageViewControllerOptionInterPageSpacingKey : 5] : nil
super.init(transitionStyle: .scroll, navigationOrientation: navigationOrientation, options: options)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Set viewcontroller by index from source
#objc func setPageIndex(_ index: Int, completion: ((Bool) -> Void)? = nil) {
guard index > 0, let source = source, index < source.count else { return }
let vc = source[index]
let direction: UIPageViewControllerNavigationDirection = index < pageIndex ? .reverse : .forward
setViewController(vc, direction: direction, completion: completion)
}
private func setViewController(_ viewController: UIViewController, direction: UIPageViewControllerNavigationDirection = .forward, completion: ((Bool) -> Void)? = nil) {
super.setViewControllers([viewController], direction: direction, animated: false, completion: completion)
}
}
extension FFPageViewController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let source = source, let index = source.index(of: viewController) else { return nil }
let count = source.count
if count == 2, index == 0 {
return nil
}
let prevIndex = (index - 1) < 0 ? count - 1 : index - 1
let pageContentViewController: UIViewController = source[prevIndex]
return pageContentViewController
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let source = source, let index = source.index(of: viewController) else { return nil }
let count = source.count
if count == 2, index == 1 {
return nil
}
let nextIndex = (index + 1) >= count ? 0 : index + 1
let pageContentViewController = source[nextIndex]
return pageContentViewController
}
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return showPageControl ? (source?.count ?? 0) : 0
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
guard showPageControl else { return 0 }
return pageIndex
}
}
The overall implementation and usage examples one can find at GitHub project.
When UIPageViewController transition, ViewController inside it(ex: UITableViewController) transition will cause crash.
In my case (crash):
step1
self.pageViewController.setViewControllers([self.tableViewController2], direction: .forward, animated: true, completion: nil)
step2
Scroll the tableView while UIPageViewController transition.
My Solution
(disable scroll both target view controller and current view controller)
self.tableViewController1.tableView.isScrollEnabled = false
self.tableViewController2.tableView.isScrollEnabled = false
self.pageViewController.setViewControllers([self.tableViewController2], direction: .forward, animated: true, completion: { _ in
self.tableViewController1.tableView.isScrollEnabled = true
self.tableViewController2.tableView.isScrollEnabled = true
})
Move your pageViewController.setViewControllers function call inside DispatchQueue.main.async block if you are doing it in code.
I don't know why it works but it worked for me. For reference.
This happened to me too when I had textfields in child controller and didn't dismiss keyboard on scroll to next controller. If this is case just add endEditing in action where you programmatically change your controller or if you are scrolling on scrollViewDidScroll delegate method of pageViewController
This happens when your UIPageViewControllerTransitionStyle is set to scroll instead of pageCurl.
Are you dynamically creating View Controllers and setting them on UIPageViewController? In that case, you must ensure that the second call to setViewControllers is called after the first one completes animation because of a bug in UIKit. A delayed dispatch is a quick and dirty fix, though it is not a good practice
More details here.
https://forums.developer.apple.com/thread/6554
For me, the issue was using self.pageViewController as a member of the current view controller instead of pageViewController as parameter obtained in the didFinishAnimating delegate method.

UITabBarController height decreases everytime it's loaded

When I first load my application and log in. Everything is fine.
However when I log out, then log back in. The height of my view has been decreased. Here's a screenshot of the bug:
I havn't been able to find the cause of this. Making this quite a difficult question to ask help with as I can't specify the precise section of code causing the issue. But I'll try.
The problematic setup is like so:
I have a containerViewController, with 2 childViewControllers, a menu and a UITabBarController. The UITabBarController has 2 UIViewControllers.
To better explain it, here's a visual representation.
_______________________
App Start ->
NavigationController(rootViewController LandingPageVC)
LandingPageVC -> push -> SignInVC(this is where I login from)
SignInVC -> push -> ContainerViewController(this has my UITabBarController and my menu)
ContainerViewController (sets up my menuTabBarController and menu)
menuTabBarController (this tabBarController is used to switch out my content from the menu)
SidePanelViewController (this is my menu)
ContainerViewController -> push(signing out) -> LandingPageVC
_______________________
Here's how I push the containerViewController when a successful login is called.
let mainTableViewController = ContainerViewController()
mainTableViewController.navigationItem.setHidesBackButton(true, animated: false)
navigationController!.pushViewController(mainTableViewController, animated: true)
menuEnabled = true
here's the function called from the containerViewController I use to log out.
func signOut() {
// Set up the landing page as the main viewcontroller again.
let mainTableViewController = LandingPageVC()
mainTableViewController.navigationItem.setHidesBackButton(true, animated: false)
mainTableViewController.skipView = false
self.navigationController!.pushViewController(mainTableViewController, animated: true)
// Disable menu access
menuEnabled = false
}
by printing the height of ContainerViewController and menuTabBarController, I found that it is the UITabBarController's height that's decreasing and not the ContainerViewController.
Here's the code that has to do with the UITabBarController
import UIKit
import QuartzCore
let menuTabBarController = UITabBarController()
var menuButton = UIBarButtonItem()
var menuEnabled = false
class ContainerViewController: UIViewController, CenterViewControllerDelegate, SidePanelViewControllerDelegate, UIGestureRecognizerDelegate {
func needsSignOut(sender: SidePanelViewController) {
// toggling left panel
self.toggleLeftPanel()
// signing out
self.signOut()
}
var centerViewController: UITabBarController!
var leftViewController: SidePanelViewController?
let centerPanelExpandedOffset: CGFloat = 60
override func viewDidLoad() {
super.viewDidLoad()
menuTabBarController.tabBar.hidden = true
menuButton = UIBarButtonItem(title: "", style: UIBarButtonItemStyle.Plain, target: self, action: "toggleLeftPanel")
if let font = UIFont(name: "FontAwesome", size: 20) {
menuButton.setTitleTextAttributes([NSFontAttributeName: font], forState: UIControlState.Normal)
}
self.navigationItem.leftBarButtonItem = menuButton
//let tabBarController = UITabBarController()
let suggestionsVC = SuggestionsViewController()
let testVC = detaiLSuggestion_VC()
let controllers = [suggestionsVC,testVC]
menuTabBarController.setViewControllers(controllers, animated: false)
centerViewController = menuTabBarController
view.addSubview(menuTabBarController.view)
addChildViewController(menuTabBarController)
//centerNavigationController.didMoveToParentViewController(self)
}
// MARK: CenterViewController delegate methods
func toggleLeftPanel() {
let notAlreadyExpanded = (currentState != .LeftPanelExpanded)
if notAlreadyExpanded {
addLeftPanelViewController()
}
animateLeftPanel(shouldExpand: notAlreadyExpanded)
}
func collapseSidePanels() {
switch (currentState) {
case .LeftPanelExpanded:
toggleLeftPanel()
default:
break
}
}
func addLeftPanelViewController() {
if (leftViewController == nil) {
leftViewController = SidePanelViewController()
leftViewController!.delegate = self
addChildSidePanelController(leftViewController!)
}
}
func addChildSidePanelController(sidePanelController: SidePanelViewController) {
view.insertSubview(sidePanelController.view, atIndex: 0)
addChildViewController(sidePanelController)
sidePanelController.didMoveToParentViewController(self)
}
func animateLeftPanel(#shouldExpand: Bool) {
if (shouldExpand) {
currentState = .LeftPanelExpanded
animateCenterPanelXPosition(targetPosition: CGRectGetWidth(centerViewController.view.frame) - centerPanelExpandedOffset)
} else {
animateCenterPanelXPosition(targetPosition: 0) { finished in
self.currentState = .BothCollapsed
self.leftViewController!.view.removeFromSuperview()
self.leftViewController = nil;
}
}
}
func animateCenterPanelXPosition(#targetPosition: CGFloat, completion: ((Bool) -> Void)! = nil) {
UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .CurveEaseInOut, animations: {
self.centerViewController.view.frame.origin.x = targetPosition
}, completion: completion)
}
Any help deducing where this is coming from or how I can go about fixing it would be greatly appreciated! And again I apologize for the dumb of code. I'll update it further if I am able to rule out parts of it.
rdelmar found a solution for me in chat.
The problem was fixed by specifying the menuTabBarController.view.frame like so:
menuTabBarController.view.frame = self.view.frame

Custom Alert (UIAlertView) with swift

How can i create a custom alert with Swift? I try translating a guide from Objective c but loads a full screen layout
for do it easy i can load a new layout with the transparent background i try this:
listaalertviewcontroller.view.backgroundColor = UIColor.clearColor()
let purple = UIColor.purpleColor() // 1.0 alpha
let semi = purple.colorWithAlphaComponent(0.5)
listaalertviewcontroller.view.backgroundColor = semi
presentingViewController.modalPresentationStyle = UIModalPresentationStyle.CurrentContext
self.presentViewController(listaalertviewcontroller, animated: true, completion: nil)
in the animation it's transparent but when the animation ends it's opaque... and i turn off opaque option in the view... what i'm doing wrong?
Code tested in Swift 5 and Xcode 10
How to make your own custom Alert
I was wanting to do something similar. First of all, UIAlertView is deprecated in favor of UIAlertController. See this answer for the standard way to display an alert:
How would I create a UIAlertView in Swift?
And both UIAlertView and UIAlertController do not really allow much customization. One option is to use some third party code. However, I discovered that it isn't that difficult to create your own Alert by displaying another view controller modaly.
The example here is just a proof-of-concept. You can design your alert any way you want.
Storyboard
You should have two View Controllers. Your second view controller will be your alert. Set the class name to AlertViewContoller and the Storyboard ID to alert. (Both of these are names that we defined ourselves in the code below, nothing special about them. You can add the code first if you want. It might actually be easier if you add the code first.)
Set the background color for the root view (in your Alert View Controller) to clear (or translucent black is nice for an alert). Add another UIView and center it with constraints. Use that as your alert background and put whatever you want inside. For my example, I added a UIButton.
Code
ViewController.swift
import UIKit
class ViewController: UIViewController {
#IBAction func showAlertButtonTapped(_ sender: UIButton) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let myAlert = storyboard.instantiateViewController(withIdentifier: "alert")
myAlert.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
myAlert.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
self.present(myAlert, animated: true, completion: nil)
}
}
AlertViewController.swift
import UIKit
class AlertViewController: UIViewController {
#IBAction func dismissButtonTapped(_ sender: UIButton) {
self.dismiss(animated: true, completion: nil)
}
}
Don't forget to hook up the outlets.
You can add an onTouchUp event listener to the background view to dismiss the popup when the user clicks outside of it.
That's it. You should be able to make any sort of alert that you can imagine now. No need for third party code.
Here is another custom alert I made. Still ugly, but it shows more things you can do.
Other options
Sometimes there is no need to reinvent the wheel, though. I'm impressed with the third party project SDCAlertView (MIT license). It is written in Swift but you can use it with Objective-C projects as well. It offers a wide range of customability.
Here is the Swift 3 code. Thanks a lot #Suragch for the awesome approach to create a custom AlertView.
ViewController.swift
import UIKit
class ViewController: UIViewController {
#IBAction func showAlertButtonTapped(sender: UIButton) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let myAlert = storyboard.instantiateViewController(withIdentifier: "storyboardID")
myAlert.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
myAlert.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
self.present(myAlert, animated: true, completion: nil)
}
AlertViewController.swift
import UIKit
class AlertViewController: UIViewController {
#IBAction func dismissButtonTapped(sender: UIButton) {
self.dismiss(animated: true, completion: nil)
}
}
To make it a little more interesting or to make the default effect in iOS, you could add either a VisualEffectView or change the color of the main UIView to a dark color and set its alpha to 70%. I prefer the second approach since the blur effect is not as smooth as the one with the view with 70 alpha.
Effect with VisualEffectView:
Effect using a UIView with 70 Alpha:
Nowadays, an alert is merely a simple presented view controller. You can write a presented view controller that behaves similarly to an alert — that is, it pops onto the screen and dims whatever is behind it — but it's your view controller and you are free to give it any interface you like.
To get you started, I've written a github project that you can download and run, and modify to suit your actual needs.
I'll show the key part of the code. The "alert" view controller, in its initializers, sets its own modal presentation style as custom and sets a transitioning delegate:
class CustomAlertViewController : UIViewController {
let transitioner = CAVTransitioner()
override init(nibName: String?, bundle: Bundle?) {
super.init(nibName: nibName, bundle: bundle)
self.modalPresentationStyle = .custom
self.transitioningDelegate = self.transitioner
}
convenience init() {
self.init(nibName:nil, bundle:nil)
}
required init?(coder: NSCoder) {
fatalError("NSCoding not supported")
}
}
All the work is done by the transitioning delegate:
class CAVTransitioner : NSObject, UIViewControllerTransitioningDelegate {
func presentationController(
forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController)
-> UIPresentationController? {
return MyPresentationController(
presentedViewController: presented, presenting: presenting)
}
}
class MyPresentationController : UIPresentationController {
func decorateView(_ v:UIView) {
// iOS 8 doesn't have this
// v.layer.borderColor = UIColor.blue.cgColor
// v.layer.borderWidth = 2
v.layer.cornerRadius = 8
let m1 = UIInterpolatingMotionEffect(
keyPath:"center.x", type:.tiltAlongHorizontalAxis)
m1.maximumRelativeValue = 10.0
m1.minimumRelativeValue = -10.0
let m2 = UIInterpolatingMotionEffect(
keyPath:"center.y", type:.tiltAlongVerticalAxis)
m2.maximumRelativeValue = 10.0
m2.minimumRelativeValue = -10.0
let g = UIMotionEffectGroup()
g.motionEffects = [m1,m2]
v.addMotionEffect(g)
}
override func presentationTransitionWillBegin() {
self.decorateView(self.presentedView!)
let vc = self.presentingViewController
let v = vc.view!
let con = self.containerView!
let shadow = UIView(frame:con.bounds)
shadow.backgroundColor = UIColor(white:0, alpha:0.4)
shadow.alpha = 0
con.insertSubview(shadow, at: 0)
shadow.autoresizingMask = [.flexibleWidth, .flexibleHeight]
let tc = vc.transitionCoordinator!
tc.animate(alongsideTransition: { _ in
shadow.alpha = 1
}) { _ in
v.tintAdjustmentMode = .dimmed
}
}
override func dismissalTransitionWillBegin() {
let vc = self.presentingViewController
let v = vc.view!
let con = self.containerView!
let shadow = con.subviews[0]
let tc = vc.transitionCoordinator!
tc.animate(alongsideTransition: { _ in
shadow.alpha = 0
}) { _ in
v.tintAdjustmentMode = .automatic
}
}
override var frameOfPresentedViewInContainerView : CGRect {
// we want to center the presented view at its "native" size
// I can think of a lot of ways to do this,
// but here we just assume that it *is* its native size
let v = self.presentedView!
let con = self.containerView!
v.center = CGPoint(x: con.bounds.midX, y: con.bounds.midY)
return v.frame.integral
}
override func containerViewWillLayoutSubviews() {
// deal with future rotation
// again, I can think of more than one approach
let v = self.presentedView!
v.autoresizingMask = [
.flexibleTopMargin, .flexibleBottomMargin,
.flexibleLeftMargin, .flexibleRightMargin
]
v.translatesAutoresizingMaskIntoConstraints = true
}
}
extension CAVTransitioner { // UIViewControllerTransitioningDelegate
func animationController(
forPresented presented:UIViewController,
presenting: UIViewController,
source: UIViewController)
-> UIViewControllerAnimatedTransitioning? {
return self
}
func animationController(
forDismissed dismissed: UIViewController)
-> UIViewControllerAnimatedTransitioning? {
return self
}
}
extension CAVTransitioner : UIViewControllerAnimatedTransitioning {
func transitionDuration(
using transitionContext: UIViewControllerContextTransitioning?)
-> TimeInterval {
return 0.25
}
func animateTransition(
using transitionContext: UIViewControllerContextTransitioning) {
let con = transitionContext.containerView
let v1 = transitionContext.view(forKey: .from)
let v2 = transitionContext.view(forKey: .to)
// we are using the same object (self) as animation controller
// for both presentation and dismissal
// so we have to distinguish the two cases
if let v2 = v2 { // presenting
con.addSubview(v2)
let scale = CGAffineTransform(scaleX: 1.6, y: 1.6)
v2.transform = scale
v2.alpha = 0
UIView.animate(withDuration: 0.25, animations: {
v2.alpha = 1
v2.transform = .identity
}) { _ in
transitionContext.completeTransition(true)
}
} else if let v1 = v1 { // dismissing
UIView.animate(withDuration: 0.25, animations: {
v1.alpha = 0
}) { _ in
transitionContext.completeTransition(true)
}
}
}
}
It looks like a lot of code, and I suppose it is, but it's almost entire confined to a single class, which is entirely boilerplate; just copy and paste. All you have to do is write the internal interface and behavior of your "alert" view controller, giving it buttons and text and whatever else you want, just as you would do for any other view controller.
Custom Alert UIView Class in swift 4. And Usage ##
import UIKit
class Dialouge: UIView {
#IBOutlet weak var lblTitle: UILabel!
#IBOutlet weak var lblDescription: UILabel!
#IBOutlet weak var btnLeft: UIButton!
#IBOutlet weak var btnRight: UIButton!
#IBOutlet weak var viewBg: UIButton!
var leftAction = {}
var rightAction = {}
override func draw(_ rect: CGRect)
{
self.btnRight.layer.cornerRadius = self.btnRight.frame.height/2
self.btnLeft.layer.cornerRadius = self.btnLeft.frame.height/2
self.btnLeft.layer.borderWidth = 1.0
self.btnLeft.layer.borderColor = #colorLiteral(red: 0.267678082, green: 0.2990377247, blue: 0.7881471515, alpha: 1)
}
#IBAction func leftAction(_ sender: Any) {
leftAction()
}
#IBAction func rightAction(_ sender: Any) {
rightAction()
}
#IBAction func bgTapped(_ sender: Any) {
self.removeFromSuperview()
}
}
strong text
## Usage Of Custom Alert with Tabbar.
let custView = Bundle.main.loadNibNamed("Dialouge", owner: self, options:
nil)![0] as? Dialouge
custView?.lblDescription.text = "Are you sure you want to delete post?"
custView?.lblTitle.text = "Delete Post"
custView?.btnLeft.setTitle("Yes", for: .normal)
custView?.btnRight.setTitle("No", for: .normal)
custView?.leftAction = {
self.deletePost(postId: self.curr_post.id,completion: {
custView?.removeFromSuperview()
})
}
custView?.rightAction = {
custView?.removeFromSuperview()
}
if let tbc = self.parentt?.tabBarController {
custView?.frame = tbc.view.frame
DispatchQueue.main.async {
tbc.view.addSubview(custView!)
}
}else if let tbc = self.parView?.parenttprof {
custView?.frame = tbc.view.frame
DispatchQueue.main.async {
tbc.view.addSubview(custView!)
}
}
else
{
custView?.frame = self.parView?.view.frame ?? CGRect.zero
DispatchQueue.main.async {
self.parView?.view.addSubview(custView!)
}
}
Use https://github.com/shantaramk/Custom-Alert-View
It is effortless to implement this. Follow the steps below:
Drag down the AlertView folder in project directory
Show AlertView Popup
func showUpdateProfilePopup(_ message: String) {
let alertView = AlertView(title: AlertMessage.success, message: message, okButtonText: LocalizedStrings.okay, cancelButtonText: "") { (_, button) in
if button == .other {
self.navigationController?.popViewController(animated: true)
}
}
alertView.show(animated: true)
}

Resources