I have chatting app. I decide to create SlideShow tutorial for it. Now I have problem. How can I run TutorialVC just once when user install the app?
Usually app starts with AuthVC. Now I want to run tutorialVC just once, and then when user close app and run it again, from auth like usually.
My tutorial VC:
class TutorialViewController: UIViewController, UIScrollViewDelegate {
#IBAction func understandButtonAction(_ sender: Any) {
}
#IBOutlet weak var understandButton: UIButton!
#IBOutlet weak var tutorialPageControl: UIPageControl!
#IBOutlet weak var tutorialScrollView: UIScrollView!
var images: [String] = ["1","2","3","4"]
var frame = CGRect(x: 0, y: 0, width: 0, height: 0)
override func viewDidLoad() {
super.viewDidLoad()
setup()
addSlider()
setupButton()
}
//===============================
//EVTAuthorizationViewController
//===============================
override func viewWillAppear(_ animated: Bool) {
UIApplication.shared.keyWindow?.windowLevel = UIWindowLevelStatusBar
}
override func viewWillDisappear(_ animated: Bool) {
UIApplication.shared.keyWindow?.windowLevel = UIWindowLevelNormal
}
//AddButton
func setupButton(){
understandButton.layer.cornerRadius = 20
}
#IBAction func buttonAction(_ sender: Any?) {
print("Successful")
}
//ScrollBars
func setup(){
self.understandButton.isHidden = true
tutorialScrollView.showsHorizontalScrollIndicator = false
tutorialScrollView.showsVerticalScrollIndicator = false
}
//ScrollView method
//=============================
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
var pageNumber = scrollView.contentOffset.x / scrollView.frame.size.width
tutorialPageControl.currentPage = Int(pageNumber)
if tutorialPageControl.currentPage == 3{
self.understandButton.isHidden = false
}else{
self.understandButton.isHidden = true
}
}
//Addslider with photo
func addSlider(){
tutorialPageControl.numberOfPages = images.count
for index in 0..<images.count{
let xPos = self.view.frame.size.width * CGFloat(index)
frame.origin.x = tutorialScrollView.frame.size.width * CGFloat(index)
//frame.size = view.frame.size
let imageView = UIImageView(frame: CGRect(x: xPos, y: 0, width: self.view.frame.width, height: self.view.frame.size.height))
imageView.image = UIImage(named: images[index])
imageView.contentMode = .scaleAspectFill
self.tutorialScrollView.addSubview(imageView)
}
tutorialScrollView.contentSize = CGSize(width: (view.frame.size.width * CGFloat(images.count)), height: view.frame.size.height)
tutorialScrollView.delegate = self
}
}
Use userDefaults. I suppose the understandButton is the button the user hits to skip the tutorial, so when when it's tapped set a true bool value for a key that you are going to use, here I've chosen "tutorial presented":
#IBAction func understandButtonAction(_ sender: Any) {
UserDefaults.standard.set(true, forKey: "tutorial presented")
}
and when the app launches, in the AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
let window = (UIApplication.shared.delegate as! AppDelegate).window
let storyboard = UIStoryboard(name: "MyStoryboardName", bundle: nil)
if UserDefaults.standard.bool(forKey: "tutorial presented") == true {
let controller = storyboard.instantiateViewController(withIdentifier: "Your Navigation controller name")
window?.rootViewController = tutorialViewController()
} else {
let tutorial = storyboard.instantiateViewController(withIdentifier: "Your tutorial controller name")
window?.rootViewController = tutorial
}
window?.makeKeyAndVisible()
return true
}
You can use a flag and store it via NSUserDefaults.
extension UserDefaults {
private static let didLaunchAppOnceKey = "didLaunchAppOnce"
var didLaunchAppOnce: Bool {
get { return bool(forKey: UserDefaults.didLaunchAppOnceKey) }
set { set(newValue, forKey: UserDefaults.didLaunchAppOnceKey) }
}
}
Then before presenting your view controller, check if the flag is set:
if !UserDefaults.standard.didLaunchAppOnce {
// Set the flag to true, so on next launch, we won't enter in the if again
UserDefaults.standard.didLaunchAppOnce = true
// Present your VC
…
}
Related
Why can't I use other pencils or colors as expected in this app? It only draws a black color. This is my code:
import UIKit
import PencilKit
import PhotosUI
class ViewController: UIViewController, PKCanvasViewDelegate, PKToolPickerObserver {
#IBOutlet weak var pencilButton: UIBarButtonItem!
#IBOutlet weak var canvasView: PKCanvasView!
let canvasWidth: CGFloat = 768
let canvasOverScrollHeight: CGFloat = 500
let drawing = PKDrawing()
override func viewDidLoad() {
super.viewDidLoad()
canvasView.drawing = drawing
canvasView.delegate = self
canvasView.alwaysBounceVertical = true
canvasView.drawingPolicy = .anyInput
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let toolPicker = PKToolPicker()
toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
canvasView.becomeFirstResponder()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let canvasScale = canvasView.bounds.width / canvasWidth
canvasView.minimumZoomScale = canvasScale
canvasView.maximumZoomScale = canvasScale
canvasView.zoomScale = canvasScale
updateContentSizeForDrawing()
canvasView.contentOffset = CGPoint(x: 0, y: -canvasView.adjustedContentInset.top)
}
override var prefersHomeIndicatorAutoHidden: Bool{
return true
}
#IBAction func fingerOrPencil (_ sender: Any) {
canvasView.allowsFingerDrawing.toggle()
pencilButton.title = canvasView.allowsFingerDrawing ? "Finger" : "Pencil"
}
#IBAction func saveToCameraRoll(_ sender: Any) {
UIGraphicsBeginImageContextWithOptions(canvasView.bounds.size, false, UIScreen.main.scale)
canvasView.drawHierarchy(in: canvasView.bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
if image != nil {
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAsset(from: image!)
}, completionHandler: {success, error in
})
}
}
func updateContentSizeForDrawing() {
let drawing = canvasView.drawing
let contentHeight: CGFloat
if !drawing.bounds.isNull {
contentHeight = max(canvasView.bounds.height, (drawing.bounds.maxY + self.canvasOverScrollHeight) * canvasView.zoomScale)
} else {
contentHeight = canvasView.bounds.height
}
canvasView.contentSize = CGSize(width: canvasWidth * canvasView.zoomScale, height: contentHeight)
}
// Delegate Methods
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
updateContentSizeForDrawing()
}
func canvasViewDidEndUsingTool(_ canvasView: PKCanvasView) {
}
func canvasViewDidFinishRendering(_ canvasView: PKCanvasView) {
}
func canvasViewDidBeginUsingTool(_ canvasView: PKCanvasView) {
}
}
These are the outputs in the console:
2023-01-04 18:34:04.429420+0300 Drawing[45460:449613] [Assert] UINavigationBar decoded as unlocked for UINavigationController, or navigationBar delegate set up incorrectly. Inconsistent configuration may cause problems. navigationController=<UINavigationController: 0x123024000>, navigationBar=<UINavigationBar: 0x12140a0a0; frame = (0 47; 0 50); opaque = NO; autoresize = W; layer = <CALayer: 0x6000030afae0>> delegate=0x123024000
2023-01-04 18:34:04.468831+0300 Drawing[45460:449613] Metal API Validation Enabled
2023-01-04 18:34:04.705019+0300 Drawing[45460:449613] [ToolPicker] Missing defaults dictionary to restore state for: PKPaletteNamedDefaults
2023-01-04 18:35:00.196200+0300 Drawing[45460:449613] Keyboard cannot present view controllers (attempted to present <UIColorPickerViewController: 0x121846e00>)
toolPicket released when out of method scope.
You should have a instance of toolPicker in ViewController.
class ViewController: UIViewController {
let toolPicker = PKToolPicker()
...
}
i solved my problem by changing
toolPicker.addObserver(self)
into
toolPicker.addObserver(canvasView)
and adding the toolPicker at the top as #noppefoxwolf suggested
I want to enable interactive modal dismissal that pans along with a users finger on a fullscreen modally presented view controller .fullscreen.
I've seen that it's fairly trivial to do so on the .pageSheet and the .formSheet which have it built in but have not seen a clear example for the full screen.
I'm guessing I'd need to have a pan gesture added to my vc within the body of it's code and then adjust for the states myself but wondering if anyone knows what exactly needs to be done / if there's a simpler way to do it as it seems much more complicated for the .fullscreen case
It can be done with creating your custom UIPresentationController and UIViewControllerTransitioningDelegate. Lets say we have TestViewController and we want to present SecondViewController with total presentedHeight of 1.0 (fullScreen). Presentation will be triggered with #IBAction func buttonPressed and can be dismissed by dragging controller down (as we are used to it). It would be also nice to add some backgroundEffect to be gradually changed while user is sliding down the SecondViewController (especially when used only presentedHeight of 0.6).
Firstly we define OverlayViewController which will be later superclass of presented SecondViewControllerand will contain UIPanGestureRecognizer.
class OverlayViewController: UIViewController {
var hasSetPointOrigin = false
var pointOrigin: CGPoint?
var delegate: OverlayViewDelegate?
override func viewDidLoad() {
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognizerAction))
view.addGestureRecognizer(panGesture)
}
override func viewDidLayoutSubviews() {
if !hasSetPointOrigin {
hasSetPointOrigin = true
pointOrigin = self.view.frame.origin
}
}
#objc func panGestureRecognizerAction(sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: view)
// Not allowing the user to drag the view upward
guard translation.y >= 0 else { return }
let currentPosition = translation.y
let originPos = self.pointOrigin
delegate?.userDragged(draggedPercentage: translation.y/originPos!.y)
// setting x as 0 because we don't want users to move the frame side ways!! Only want straight up or down
view.frame.origin = CGPoint(x: 0, y: self.pointOrigin!.y + translation.y)
if sender.state == .ended {
let dragVelocity = sender.velocity(in: view)
if dragVelocity.y >= 1100 {
self.dismiss(animated: true, completion: nil)
} else {
// Set back to original position of the view controller
UIView.animate(withDuration: 0.3) {
self.view.frame.origin = self.pointOrigin ?? CGPoint(x: 0, y: 400)
self.delegate?.animateBlurBack(seconds: 0.3)
}
}
}
}
}
protocol OverlayViewDelegate: AnyObject {
func userDragged(draggedPercentage: CGFloat)
func animateBlurBack(seconds: TimeInterval)
}
Next we define custom PresentationController
class PresentationController: UIPresentationController {
private var backgroundEffectView: UIView?
private var backgroundEffect: BackgroundEffect?
private var viewHeight: CGFloat?
private let maxDim:CGFloat = 0.6
private var tapGestureRecognizer: UITapGestureRecognizer = UITapGestureRecognizer()
convenience init(presentedViewController: UIViewController,
presenting presentingViewController: UIViewController?,
backgroundEffect: BackgroundEffect = .blur,
viewHeight: CGFloat = 0.6)
{
self.init(presentedViewController: presentedViewController, presenting: presentingViewController)
self.backgroundEffect = backgroundEffect
self.backgroundEffectView = returnCorrectEffectView(backgroundEffect)
self.tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissController))
self.backgroundEffectView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.backgroundEffectView?.isUserInteractionEnabled = true
self.backgroundEffectView?.addGestureRecognizer(tapGestureRecognizer)
self.viewHeight = viewHeight
}
private override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
override var frameOfPresentedViewInContainerView: CGRect {
CGRect(origin: CGPoint(x: 0, y: self.containerView!.frame.height * (1-viewHeight!)),
size: CGSize(width: self.containerView!.frame.width, height: self.containerView!.frame.height *
viewHeight!))
}
override func presentationTransitionWillBegin() {
self.backgroundEffectView?.alpha = 0
self.containerView?.addSubview(backgroundEffectView!)
self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
switch self.backgroundEffect! {
case .blur:
self.backgroundEffectView?.alpha = 1
case .dim:
self.backgroundEffectView?.alpha = self.maxDim
case .none:
self.backgroundEffectView?.alpha = 0
}
}, completion: { (UIViewControllerTransitionCoordinatorContext) in })
}
override func dismissalTransitionWillBegin() {
self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
self.backgroundEffectView?.alpha = 0
}, completion: { (UIViewControllerTransitionCoordinatorContext) in
self.backgroundEffectView?.removeFromSuperview()
})
}
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
}
override func containerViewDidLayoutSubviews() {
super.containerViewDidLayoutSubviews()
presentedView?.frame = frameOfPresentedViewInContainerView
backgroundEffectView?.frame = containerView!.bounds
}
#objc func dismissController(){
self.presentedViewController.dismiss(animated: true, completion: nil)
}
func graduallyChangeOpacity(withPercentage: CGFloat) {
self.backgroundEffectView?.alpha = withPercentage
}
func returnCorrectEffectView(_ effect: BackgroundEffect) -> UIView {
switch effect {
case .blur:
var blurEffect = UIBlurEffect(style: .dark)
if self.traitCollection.userInterfaceStyle == .dark {
blurEffect = UIBlurEffect(style: .light)
}
return UIVisualEffectView(effect: blurEffect)
case .dim:
var dimView = UIView()
dimView.backgroundColor = .black
if self.traitCollection.userInterfaceStyle == .dark {
dimView.backgroundColor = .gray
}
dimView.alpha = maxDim
return dimView
case .none:
let clearView = UIView()
clearView.backgroundColor = .clear
return clearView
}
}
}
extension PresentationController: OverlayViewDelegate {
func userDragged(draggedPercentage: CGFloat) {
graduallyChangeOpacity(withPercentage: 1-draggedPercentage)
switch self.backgroundEffect! {
case .blur:
graduallyChangeOpacity(withPercentage: 1-draggedPercentage)
case .dim:
graduallyChangeOpacity(withPercentage: maxDim-draggedPercentage)
case .none:
self.backgroundEffectView?.alpha = 0
}
}
func animateBlurBack(seconds: TimeInterval) {
UIView.animate(withDuration: seconds) {
switch self.backgroundEffect! {
case .blur:
self.backgroundEffectView?.alpha = 1
case .dim:
self.backgroundEffectView?.alpha = self.maxDim
case .none:
self.backgroundEffectView?.alpha = 0
}
}
}
}
enum BackgroundEffect {
case blur
case dim
case none
}
Create SecondViewController subclassing OverlayViewController:
class SecondViewController: OverlayViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .blue
// Do any additional setup after loading the view.
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
addSlider()
}
func addSlider() {
let sliderWidth:CGFloat = 100
let centerOfScreen = self.view.frame.size.width / 2
let rect = CGRect(x: centerOfScreen - sliderWidth/2, y: 80, width: sliderWidth, height: 10)
let slider = UIView(frame: rect)
slider.backgroundColor = .black
self.view.addSubview(slider)
}
Add showOverlay() function that will be triggered after buttonPressed and conform your presenting UIViewController (TestViewController) to UIViewControllerTransitioningDelegate :
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func buttonPressed(_ sender: Any) {
showOverlay()
}
func showOverlay() {
let secondVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "secondVC") as! SecondViewController
secondVC.modalPresentationStyle = .custom
secondVC.transitioningDelegate = self
self.present(secondVC, animated: true, completion: nil)
}
}
extension TestViewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController) -> UIPresentationController?
{
let presentedHeight: CGFloat = 1.0
let controller = PresentationController(presentedViewController: presented,
presenting: presenting,
backgroundEffect: .dim,
viewHeight: presentedHeight)
if let vc = presented as? OverlayViewController {
vc.delegate = controller
}
return controller
}
}
Now we should be able to present SecondViewController with showOverlay() function setting its presentedHeight to 1.0 and .dim background effect. We can dismiss SecondViewController similar to another modal presentations.
I'm currently trying to implement a Map connected with a search function. For the overlay containing the table view, I've decided to go for a library called FloatingPanel. Anyways, this shouldn't be of importance, since it shouldn't affect the essential code etc.
The data is being read inside of SearchResultsTableViewController and passed to MapViewController by passData().
Inside of passData() a function called addAnnotationToMap() from MapViewController is being called to process the data - printing inside of the function will always return the correct value.
Inside of the function I'm trying to add an annotation to the map, but it won't work. Nothing happens - but when I do print(mapView.annotations) the Array of annotations is being returned including mine.
By the way, I had to add loadViewIfNeeded() to the MapViewController because without (after using the overlay view) mapView returned nil.
Sorry for the amount of code - but there might be some relevant code. I didn't include the code for the table view.
MapViewController
class MapViewController: UIViewController, FloatingPanelControllerDelegate, UISearchBarDelegate {
var fpc: FloatingPanelController!
var searchVC = SearchResultTableViewController()
private enum AnnotationReuseID: String {
case pin
}
#IBOutlet private var mapView: MKMapView!
var mapItems: [MKMapItem]?
var boundingRegion: MKCoordinateRegion?
override func viewDidLoad() {
super.viewDidLoad()
if let region = boundingRegion {
mapView.region = region
}
fpc = FloatingPanelController()
fpc.delegate = self
// Initialize FloatingPanelController and add the view
fpc.surfaceView.backgroundColor = .clear
fpc.surfaceView.cornerRadius = 9.0
fpc.surfaceView.shadowHidden = false
searchVC = (storyboard?.instantiateViewController(withIdentifier: "SearchPanel") as! SearchResultTableViewController)
// Set a content view controller
fpc.set(contentViewController: searchVC)
fpc.track(scrollView: searchVC.tableView)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Add FloatingPanel to a view with animation.
fpc.addPanel(toParent: self, animated: true)
fpc.move(to: .tip, animated: true)
// Must be here
searchVC.searchController.searchBar.delegate = self
}
func addAnnotationToMap() {
loadViewIfNeeded()
guard let item = mapItems?.first else { return }
guard let coordinate = item.placemark.location?.coordinate else { return }
let annotation = MKPointAnnotation()
annotation.title = item.name
annotation.coordinate = coordinate
mapView.addAnnotation(annotation)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searchBar.resignFirstResponder()
searchBar.showsCancelButton = false
fpc.move(to: .tip, animated: true)
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
searchBar.showsCancelButton = true
searchVC.tableView.alpha = 1.0
fpc.move(to: .full, animated: true)
searchVC.hideHeader()
}
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
fpc.move(to: .half, animated: true)
searchVC.showHeader()
}
func floatingPanelDidMove(_ vc: FloatingPanelController) {
let y = vc.surfaceView.frame.origin.y
let tipY = vc.originYOfSurface(for: .tip)
if y > tipY - 44.0 {
let progress = max(0.0, min((tipY - y) / 44.0, 1.0))
self.searchVC.tableView.alpha = progress
}
}
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {
if vc.position == .full {
searchVC.searchBar.showsCancelButton = false
searchVC.searchBar.resignFirstResponder()
}
}
func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) {
UIView.animate(withDuration: 0.25,
delay: 0.0,
options: .allowUserInteraction,
animations: {
if targetPosition == .tip {
self.searchVC.tableView.alpha = 0.0
self.searchVC.hideHeader()
} else if targetPosition == .half {
self.searchVC.tableView.alpha = 1.0
self.searchVC.showHeader()
} else {
self.searchVC.tableView.alpha = 1.0
self.searchVC.hideHeader()
}
}, completion: nil)
}
}
SearchViewController
class SearchResultTableViewController: UIViewController {
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var visualEffectView: UIVisualEffectView!
private enum CellReuseID: String {
case resultCell
}
private var places: [MKMapItem]? {
didSet {
tableView.reloadData()
}
}
private var suggestionController: SuggestionsTableTableViewController!
var searchController: UISearchController!
private var localSearch: MKLocalSearch? {
willSet {
places = nil
localSearch?.cancel()
}
}
private var boundingRegion: MKCoordinateRegion?
override func awakeFromNib() {
super.awakeFromNib()
suggestionController = SuggestionsTableTableViewController()
suggestionController.tableView.delegate = self
searchController = UISearchController(searchResultsController: suggestionController)
searchController.searchResultsUpdater = suggestionController
searchController.searchBar.isUserInteractionEnabled = false
searchController.searchBar.alpha = 0.5
}
override func viewDidLoad() {
super.viewDidLoad()
searchBar.addSubview(searchController.searchBar)
searchController.searchBar.searchBarStyle = .minimal
searchController.searchBar.tintColor = .black
searchController.searchBar.isUserInteractionEnabled = true
searchController.dimsBackgroundDuringPresentation = false
definesPresentationContext = true
hideHeader()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 10, *) {
visualEffectView.layer.cornerRadius = 9.0
visualEffectView.clipsToBounds = true
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
func showHeader() {
changeHeader(height: 116.0)
}
func hideHeader() {
changeHeader(height: 0.0)
}
func changeHeader(height: CGFloat) {
tableView.beginUpdates()
if let headerView = tableView.tableHeaderView {
UIView.animate(withDuration: 0.25) {
var frame = headerView.frame
frame.size.height = height
self.tableView.tableHeaderView?.frame = frame
}
}
tableView.endUpdates()
}
func passData() {
guard let mapViewController = storyboard?.instantiateViewController(withIdentifier: "map") as? MapViewController else { return }
guard let mapItem = places?.first else { return }
guard let coordinate = mapItem.placemark.location?.coordinate else { return }
let span = MKCoordinateSpan(latitudeDelta: coordinate.latitude, longitudeDelta: coordinate.longitude)
let region = MKCoordinateRegion(center: coordinate, span: span)
mapViewController.boundingRegion = region
mapViewController.mapItems = [mapItem]
mapViewController.addAnnotationToMap()
}
private func search(for suggestedCompletion: MKLocalSearchCompletion) {
let searchRequest = MKLocalSearch.Request(completion: suggestedCompletion)
search(using: searchRequest)
}
private func search(for queryString: String?) {
let searchRequest = MKLocalSearch.Request()
searchRequest.naturalLanguageQuery = queryString
search(using: searchRequest)
}
private func search(using searchRequest: MKLocalSearch.Request) {
if let region = boundingRegion {
searchRequest.region = region
}
UIApplication.shared.isNetworkActivityIndicatorVisible = true
localSearch = MKLocalSearch(request: searchRequest)
localSearch?.start { [weak self] (response, error) in
if error == nil {
self?.passData()
} else {
self?.displaySearchError(error)
return
}
self?.places = response?.mapItems
self?.boundingRegion = response?.boundingRegion
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}
}
private func displaySearchError(_ error: Error?) {
if let error = error as NSError?, let errorString = error.userInfo[NSLocalizedDescriptionKey] as? String {
let alertController = UIAlertController(title: "Could not find any places.", message: errorString, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
present(alertController, animated: true, completion: nil)
}
}
}
I have a Child UICollectionViewController where I have an array of images.
When I delete any photo I want to send back that array of updated images to Parent UIViewController.
Also in Child controller I have a programatically view which is called when I click on any image to expand it. When the image is expanded the user can click on a Delete button to delete photos from that array.
My array is updated correctly after delete but I can't manage to send it back to parent for some reasons.
I tried to send it back using Delegates and Protocols.
Here is my code for child controller:
protocol ListImagesDelegate {
func receiveImagesUpdated(data: [String]?)
}
class ListImagesVC: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
// Properties
var receivedImagesPath: [String]? = []
var fullscreenImageView = UIImageView()
var indexOfSelectedImage = 0
var imagesAfterDelete: [String]? = []
var delegate: ListImagesDefectDelegate?
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
print("imagesAfterDelete: \(imagesAfterDelete ?? [])") // I'm getting the right number of images in this array.
delegate?.receiveImagesUpdated(data: imagesAfterDelete)
}
...
...
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Click on photo: \(indexPath.item + 1)")
if let imagePath = receivedImagesPath?[indexPath.item] {
guard let selectedImage = loadImageFromDiskWith(fileName: imagePath) else {return}
setupFullscreenImageView(image: selectedImage)
indexOfSelectedImage = indexPath.item
}
}
private func setupFullscreenImageView(image: UIImage){
fullscreenImageView = UIImageView(image: image)
fullscreenImageView.frame = UIScreen.main.bounds
fullscreenImageView.backgroundColor = .black
fullscreenImageView.contentMode = .scaleAspectFit
fullscreenImageView.isUserInteractionEnabled = true
self.view.addSubview(fullscreenImageView)
self.navigationController?.isNavigationBarHidden = true
self.tabBarController?.tabBar.isHidden = true
let deleteButton = UIButton(frame: CGRect(x: fullscreenImageView.bounds.maxX - 50, y: fullscreenImageView.bounds.maxY - 75, width: 30, height: 40))
deleteButton.autoresizingMask = [.flexibleLeftMargin, .flexibleBottomMargin]
deleteButton.backgroundColor = .black
deleteButton.setImage(UIImage(named: "trashIcon"), for: .normal)
deleteButton.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside)
fullscreenImageView.addSubview(deleteButton)
}
#objc func deleteButtonTapped(button: UIButton!) {
print("Delete button tapped")
receivedImagesPath?.remove(at: indexOfSelectedImage)
imagesAfterDelete = receivedImagesPath
collectionView.reloadData()
self.navigationController?.isNavigationBarHidden = false
self.tabBarController?.tabBar.isHidden = false
fullscreenImageView.removeFromSuperview()
}
Here is the Parent controller:
var updatedImages: [String]? = []
...
...
extension NewAlbumVC: ListImagesDelegate {
func receiveImagesUpdated(data: [String]?) {
print("New array: \(data ?? [])") // This print is never called.
updatedImages = data
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "goToImages" {
let listImagesVC = segue.destination as! ListImagesVC
listImagesVC.delegate = self
}
}
}
I want to specify that my child controller have set a Storyboard ID ("ListImagesID") and also a segue identifier from parent to child ("goToImages"). Can cause this any conflict ?
Thanks if you read this.
It appears that the delegate is nil here
delegate?.receiveImagesUpdated(data: imagesAfterDelete)
For this
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
to trigger you must have
self.performSegue(withIdentifier:"goToImages",sender:nil)
Edit: This
let listImagesDefectVC = storyboard?.instantiateViewController(withIdentifier: "ListImagesDefectID") as! ListImagesDefectVC
listImagesDefectVC.receivedImagesPath = imagesPath
navigationController?.pushViewController(listImagesDefectVC, animated: true)
doesn't trigger prepareForSegue , so add
listImagesDefectV.delegate = self
So finally
Solution 1 :
#objc func tapOnImageView() {
let listImagesDefectVC = storyboard?.instantiateViewController(withIdentifier: "ListImagesDefectID") as! ListImagesDefectVC
listImagesDefectVC.receivedImagesPath = imagesPath
listImagesDefectVC.delegate = self
navigationController?.pushViewController(listImagesDefectVC, animated: true)
}
Solution 2 :
#objc func tapOnImageView() {
self.performSegue(withIdentifier:"goToImages",sender:nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "goToImages" {
let listImagesVC = segue.destination as! ListImagesVC
listImagesVC.receivedImagesPath = imagesPath
listImagesVC.delegate = self
}
}
Problem
When tapping the skip button on page i (which calls setViewControllers(_:animated:) and transitions the user to the last page in the page view controller), and then swiping back to page i again, the page control disappears.
Wanted result
I want to programmatically add and show a custom page control on the bottom of each view controller in a page view controller when said page view controller contains different types of view controllers.
Efforts so far to resolve the issue
Adding the page control to the base view controller each time it appears.
Calling loadView() on the view controller that contains the missing page control.
Code
I have a WalkthroughRootViewController that contains a UIPageViewController. The type of the view controllers in the page view controller are two subclasses of type WalkthroughBaseViewController, the first n-1 of one type, and the last of the other. I have not included code of the last type, as that's working as far as I can see.
I have this code in WalkthroughBaseViewController:
lazy var pageControl: UIPageControl = {
let pageControl = UIPageControl(frame: .zero)
pageControl.translatesAutoresizingMaskIntoConstraints = false
pageControl.numberOfPages = numberOfPages
pageControl.sizeToFit()
pageControl.pageIndicatorTintColor = Colors.brown
pageControl.currentPageIndicatorTintColor = Colors.silver
pageControl.isUserInteractionEnabled = false
pageControl.isEnabled = false
return pageControl
}()
The page control is added to the view in viewDidLoad():
view.addSubview(pageControl)
NSLayoutConstraint.activate([
pageControl.bottomAnchor.constraint(equalTo: view.bottomAnchor),
pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
If the user is on any of the first n-1 view controllers, there is a skip button the user can tap to skip forward to the last view controller. The code for this is
func skipWalkthrough() {
guard let viewController = walkthroughPageViewControllerDataSource.viewController(at: lastIndex, storyboard: storyboard!) else { return }
walkthroughPageViewController.setViewControllers([viewController], direction: .forward, animated: true)
}
Reference
I have highlighted the code I believe is important, but here are all files related to the walkthrough of the application.
WalkthroughRootViewController
import UIKit
class WalkthroughRootViewController: UIViewController {
// MARK: Regular Properties
var walkthroughPageViewController: UIPageViewController!
var walkthroughImages = [
Images.w1,
Images.w2
]
var walkthroughStrings: [String] = [
.localized(.walkthroughTitle1),
.localized(.walkthroughZipCodeTitle)
]
// MARK: Lazy Properties
lazy var walkthroughPageViewControllerDataSource: WalkthroughPageViewControllerDataSource = {
var dataSource = WalkthroughPageViewControllerDataSource()
dataSource.walkthroughRootViewController = self
return dataSource
}()
// MARK: Computed Properties
var lastIndex: Int {
return walkthroughImages.count - 1
}
var temporaryUserInput: String?
var temporarySwitchPosition = false
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
}
// MARK: View Controller Life Cycle
extension WalkthroughRootViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Stop gray shadow from appearing under transition.
navigationController?.view.backgroundColor = .white
configurePageViewController()
}
}
// MARK: Helper Methods
extension WalkthroughRootViewController {
func configurePageViewController() {
walkthroughPageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
walkthroughPageViewController.dataSource = walkthroughPageViewControllerDataSource
walkthroughPageViewController.delegate = walkthroughPageViewControllerDataSource
let startingViewController = storyboard!.instantiateViewController(withIdentifier: Strings.ViewControllerIdentifiers.walkthroughImage) as! WalkthroughImageViewController
let startIndex = 0
startingViewController.delegate = self
startingViewController.pageIndex = startIndex
startingViewController.text = walkthroughStrings[startIndex]
startingViewController.image = walkthroughImages[startIndex]
startingViewController.numberOfPages = walkthroughImages.count
walkthroughPageViewController.setViewControllers([startingViewController], direction: .forward, animated: true)
walkthroughPageViewController.view.frame = view.bounds
add(walkthroughPageViewController)
}
}
extension WalkthroughRootViewController: WalkthroughDelegate {
func skipWalkthrough() {
guard let viewController = walkthroughPageViewControllerDataSource.viewController(at: lastIndex, storyboard: storyboard!) else { return }
walkthroughPageViewController.setViewControllers([viewController], direction: .forward, animated: true)
}
}
extension WalkthroughRootViewController: WalkthrouZipCodeViewControllerDelegate {
func walkththroughZipCodeViewController(_ viewController: WalkthroughZipCodeViewController, userEnteredText enteredText: String) {
temporaryUserInput = enteredText
}
func walkthroughZipCodeViewController(_ viewController: WalkthroughZipCodeViewController, userChangedSwitchPosition position: Bool) {
temporarySwitchPosition = position
}
}
WalkthroughBaseViewController
import UIKit
protocol WalkthroughDelegate: class {
func skipWalkthrough()
}
class WalkthroughBaseViewController: UIViewController {
// MARK: Regular Properties
var pageIndex = 0
var text = ""
var delegate: WalkthroughDelegate?
var numberOfPages = 0
// Lazy Properties
lazy var pageControl: UIPageControl = {
let pageControl = UIPageControl(frame: .zero)
pageControl.translatesAutoresizingMaskIntoConstraints = false
pageControl.numberOfPages = numberOfPages
pageControl.sizeToFit()
pageControl.pageIndicatorTintColor = Colors.brown
pageControl.currentPageIndicatorTintColor = Colors.silver
pageControl.isUserInteractionEnabled = false
pageControl.isEnabled = false
return pageControl
}()
}
// MARK: View Controller Life Cycle
extension WalkthroughBaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = Colors.silver
view.addSubview(pageControl)
NSLayoutConstraint.activate([
pageControl.bottomAnchor.constraint(equalTo: view.bottomAnchor),
pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
view.accessibilityIdentifier = Strings.AccessibilityIdentifiers.walkthrough
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
pageControl.currentPage = pageIndex
}
}
WalkthroughImageViewController
import UIKit
class WalkthroughImageViewController: WalkthroughBaseViewController {
// MARK: #IBOutlets
#IBOutlet weak var titleLabel: UILabel! {
didSet {
titleLabel.adjustsFontSizeToFitWidth = true
titleLabel.textColor = Colors.silver
titleLabel.numberOfLines = 0
}
}
#IBOutlet weak var skipWalkthroughButton: UIButton! {
didSet {
skipWalkthroughButton.setTitleColor(Colors.silver, for: .normal)
skipWalkthroughButton.titleLabel?.font = UIFont.preferredBoldFont(for: .body)
skipWalkthroughButton.setTitle(.localized(.skip), for: .normal)
}
}
#IBOutlet weak var imageView: UIImageView! {
didSet {
imageView.layer.shadowColor = Colors.brown.cgColor
imageView.layer.shadowOffset = CGSize(width: 0, height: 1)
imageView.layer.shadowOpacity = 1
imageView.layer.shadowRadius = 1.0
imageView.clipsToBounds = false
imageView.contentMode = .scaleAspectFill
}
}
// MARK: Regular Properties
var image: UIImage?
// MARK: View Controller Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = image
titleLabel.text = text
}
}
// MARK: #IBActions
extension WalkthroughImageViewController {
#IBAction func skipWalkthrough(_ sender: UIButton) {
delegate?.skipWalkthrough()
}
}
WalkthroughPageViewControllerDataSource
import UIKit
class WalkthroughPageViewControllerDataSource: NSObject {
// MARK: Regular Properties
var walkthroughRootViewController: WalkthroughRootViewController!
}
extension WalkthroughPageViewControllerDataSource: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
var index = indexOfViewController(viewController as! WalkthroughBaseViewController)
if index == NSNotFound || index == 0 {
return nil
}
index -= 1
return self.viewController(at: index, storyboard: walkthroughRootViewController.storyboard!)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
var index = indexOfViewController(viewController as! WalkthroughBaseViewController)
if index == NSNotFound {
return nil
}
index += 1
if index == walkthroughRootViewController.walkthroughImages.count {
return nil
}
return self.viewController(at: index, storyboard: walkthroughRootViewController.storyboard!)
}
}
extension WalkthroughPageViewControllerDataSource {
func viewController(at index: Int, storyboard: UIStoryboard) -> WalkthroughBaseViewController? {
if walkthroughRootViewController.walkthroughImages.count == 0 || index >= walkthroughRootViewController.walkthroughImages.count {
return nil
}
var viewController: WalkthroughBaseViewController?
if index == walkthroughRootViewController.lastIndex {
viewController = storyboard.instantiateViewController(withIdentifier: Strings.ViewControllerIdentifiers.walkthroughZipCode) as? WalkthroughZipCodeViewController
if let viewController = viewController as? WalkthroughZipCodeViewController {
viewController.pageIndex = index
viewController.walkthroughZipCodeDelegate = walkthroughRootViewController
viewController.temporaryUserInput = walkthroughRootViewController.temporaryUserInput
viewController.temporarySwitchPosition = walkthroughRootViewController.temporarySwitchPosition
viewController.numberOfPages = walkthroughRootViewController.walkthroughImages.count
viewController.image = walkthroughRootViewController.walkthroughImages[index]
}
} else {
viewController = storyboard.instantiateViewController(withIdentifier: Strings.ViewControllerIdentifiers.walkthroughImage) as? WalkthroughImageViewController
if let viewController = viewController as? WalkthroughImageViewController {
viewController.delegate = walkthroughRootViewController
viewController.pageIndex = index
viewController.image = walkthroughRootViewController.walkthroughImages[index]
viewController.text = walkthroughRootViewController.walkthroughStrings[index]
}
}
return viewController
}
func indexOfViewController(_ viewController: WalkthroughBaseViewController) -> Int {
return viewController.pageIndex
}
}
extension WalkthroughPageViewControllerDataSource: UIPageViewControllerDelegate {
}
Create a single UIPageControl that you put in the WalkthroughRootViewController and update it when you navigate the pages - don't create a page control for each child.
Try not to use extensions to override methods - it can cause you trouble - see this blog entry.