Why is superview returning nil? [duplicate] - swift

This question already has answers here:
Displaying instances of view controllers within Xcode without disturbing the current hierarchy
(1 answer)
Adding a view controller as a subview in another view controller
(8 answers)
Closed 1 year ago.
At the very bottom is my code where MainController initiates a subview called setController. However, within setController's code I get back a nil when I try to the superview:
override func viewDidLoad() {
super.viewDidLoad()
print(self.view.superview ?? "no parent")
}
I am assigning the second view as a subview, but obviously I am missing something. Have I misunderstood how UIView hierarchy works?
class MainController: UIViewController {
private lazy var setController = SetController()
var invButton : MyButton!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
invButton = makeButton(vControl: self, btype: ButtType.inv, action: #selector(self.buttonAction(sender:)))
invButton.frame.origin.x = self.view.frame.width * 0.1
invButton.frame.origin.y = self.view.frame.height * 0.1
invButton.setTitle("Settings", for: .normal)
}
override var prefersStatusBarHidden: Bool {
return false
}
#objc func buttonAction(sender: UIButton!) {
guard let theButton = sender as? MyButton else { return}
UIView.transition(with: self.view, duration: 0.5, options: .transitionCurlDown, animations: { [self] in
self.view.addSubview(setController.view)
}, completion: nil)
}
}

For anyone coming across this problem as well, I found the answer here:
https://guides.codepath.com/ios/Adding-and-Removing-Child-View-Controllers
The error was mine in that I didn't realize that adding a subview was a multiple line process.
So self.view.addSubview(setController.view) became:
addChild(setController);
view.addSubview(setController.view);
setController.didMove(toParent: self);

Related

When minimizing a view, root controller contents also minimize swift

Hi there I am trying to recreate apple musics miniplayer controller. There is a view that shows details about the song playing such as the song name, artist name, cover art and so forth like apple music. When a user clicks the dismiss button on the top of that controller, it minimizes it to a view just above the tabBar revealing the rootview behind the view. The only problem is that my code is causing an issue that when the view is minimized, instead of minimizing the view that shows the information about the current song being played, it minimizes all the views and leaves just a black screen. I'm not sure what is causing the issue but I will provide the code for my tabBar controller which houses the code to minimize and maximize the view and then the other controller which calls the function created in the tabBar controller to minimze and maximize the view as well as screen shots of what is happening. Thank you for taking the time to look at this. If anything is unclear please let me know.
TabBarController Code:
import Foundation
import UIKit
import Firebase
class TabBarController: UITabBarController {
var user: User? {
didSet {
guard let nav = viewControllers?[0] as? UINavigationController else { return }
guard let feed = nav.viewControllers.first as? FeedController else { return }
feed.user = user
}
}
override func viewDidLoad() {
super.viewDidLoad()
fetchUser()
setupDetailsPlayerView()
// perform(#selector(minimizePlayerDetails), with: nil, afterDelay: 1)
// perform(#selector(maximizePlayerDetails), with: nil, afterDelay: 1)
}
#objc func minimizePlayerDetails() {
maximizeTopAnchorConstraint.isActive = false
minimizeTopAnchorConstraint.isActive = true
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
// self.view.layoutIfNeeded()
})
}
#objc func maximizePlayerDetails() {
maximizeTopAnchorConstraint.isActive = true
maximizeTopAnchorConstraint.constant = 0
minimizeTopAnchorConstraint.isActive = false
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.view.layoutIfNeeded()
})
}
func fetchUser() {
guard let uid = Auth.auth().currentUser?.uid else { return }
UserService.shared.fetchUser(uid: uid) { user in
self.user = user
}
}
let playerDetailsView = PlayerDetailController.initFromNib()
var maximizeTopAnchorConstraint: NSLayoutConstraint!
var minimizeTopAnchorConstraint: NSLayoutConstraint!
fileprivate func setupDetailsPlayerView() {
print("setting up details view")
// view.addSubview(playerDetailsView)
view.insertSubview(playerDetailsView, belowSubview: tabBar)
playerDetailsView.translatesAutoresizingMaskIntoConstraints = false
maximizeTopAnchorConstraint = playerDetailsView.topAnchor.constraint(equalTo: view.topAnchor, constant: view.frame.height)
maximizeTopAnchorConstraint.isActive = true
minimizeTopAnchorConstraint = playerDetailsView.topAnchor.constraint(equalTo: tabBar.topAnchor, constant: -64)
// minimizeTopAnchorConstraint.isActive = true
playerDetailsView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
playerDetailsView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
playerDetailsView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
}
}
function being called in songcontroller:
#IBAction func dismissTapped(_ sender: Any) {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let sceneDelegate = windowScene.delegate as? SceneDelegate
else {
return
}
let viewController = TabBarController()
sceneDelegate.window?.rootViewController = viewController
viewController.minimizePlayerDetails()
print("clicked")
self.removeFromSuperview()
}
Screenshots of what is happening:
Normal View:
Minimized View:
If you've created the TabBarController by storyboard then the empty initialization you've provided won't work. You need to instantiate the UIViewController from the storyboard. This seems to be the reason why you're getting an empty UITabBarController when setting the rootViewController. Modify your button action dismissTapped like this:
#IBAction func dismissTapped(_ sender: Any) {
//...
let viewController = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier: "TabBarController") as? TabBarController
sceneDelegate.window?.rootViewController = viewController
//...
}

Swift - Dismiss Keyboard and call TouchUpInside Button Simultaneously not working when using UIKeyboardWillChangeFrame

I am using swift and having issues with TouchUpInside: if I'm using UIKeyboardWillChangeFrame or UIKeyboardWillShow/UIKeyboardWillHide, & the keyboard is showing, & the button I'm trying to press is behind the keyboard when keyboard is shown initially. (If I scroll down to the button till visible and press, no touchUpInside called).
TouchDown seems to work consistently whether the keyboard is showing or not, but TouchUpInside is not called. If the button is above the top of the keyboard when the keyboard is initially shown, TouchUpInside works. I'm using keyboardNotification to set the height of a view below my scrollView in order to raise up my scrollView when keyboard is showing. From what I can see it's only usually when the button is the last element in the scrollView (and therefore likely to be behind the keyboard when keyboard shown).
#IBOutlet var keyboardHeightLayoutConstraint: NSLayoutConstraint?
#IBOutlet weak var textField: UITextField!
#IBOutlet weak var saveButton: UIButton!
#IBAction func saveTouchUpInside(_ sender: UIButton) {
print("touchupinside = does not work")
}
#objc func saveTouchDown(notification:NSNotification){
print("touchdown = works")
}
viewWillAppear:
textField.delegate = self
NotificationCenter.default.addObserver(self,selector:#selector(self.keyboardNotification(notification:)),name:
NSNotification.Name.UIKeyboardWillChangeFrame,object: nil)
self.saveButton.addTarget(self, action:#selector(ViewController.saveTouchDown(notification:)), for: .touchDown)
deinit {
NotificationCenter.default.removeObserver(self)
}
#objc func keyboardNotification(notification: NSNotification) {
if let userInfo = notification.userInfo {
let endFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
let endFrameY = endFrame?.origin.y ?? 0
let duration:TimeInterval = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0
let animationCurveRawNSN = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber
let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIViewAnimationOptions.curveEaseInOut.rawValue
let animationCurve:UIViewAnimationOptions = UIViewAnimationOptions(rawValue: animationCurveRaw)
if endFrameY >= UIScreen.main.bounds.size.height {
self.keyboardHeightLayoutConstraint?.constant = 0.0
} else {
self.keyboardHeightLayoutConstraint?.constant = endFrame?.size.height ?? 0.0
}
UIView.animate(withDuration: duration, delay: TimeInterval(0),options: animationCurve, animations: { self.view.layoutIfNeeded() }, completion: nil)
}
}
I would like to dismiss the keyboard and call saveTouchUpInside at the same time, without using TouchDown.
I abstract the keyboard interaction as a separate class so that my controllers do not get bloated(also follows separation of concerns). Here is the keyboard manager class that I use.
import UIKit
/**
* To adjust the scroll view associated with the displayed view to accommodate
* the display of keyboard so that the view gets adjusted accordingly without getting hidden
*/
class KeyboardManager {
private var scrollView: UIScrollView
/**
* -parameter scrollView: ScrollView that need to be adjusted so that it does not get clipped by the presence of the keyboard
*/
init(scrollView: UIScrollView) {
self.scrollView = scrollView
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self,
selector: #selector(adjustForKeyboard),
name: UIResponder.keyboardWillHideNotification, object: nil)
notificationCenter.addObserver(self,
selector: #selector(adjustForKeyboard),
name: UIResponder.keyboardDidChangeFrameNotification, object: nil)
}
/**
* Indicates that the on-screen keyboard is about to be presented.
* -parameter notification: Contains animation and frame details on the keyboard
*
*/
#objc func adjustForKeyboard(notification: Notification) {
guard let containedView = scrollView.superview else { return }
let userInfo = notification.userInfo!
let keyboardScreenEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
let keyboardViewEndFrame = containedView.convert(keyboardScreenEndFrame, to: containedView.window)
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber
let rawAnimationCurveValue = (userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as! NSNumber).uintValue
UIView.animate(withDuration: TimeInterval(truncating: duration),
delay: 0,
options: [UIView.AnimationOptions(rawValue: rawAnimationCurveValue)],
animations: {
if notification.name == UIResponder.keyboardWillHideNotification {
self.scrollView.contentInset = UIEdgeInsets.zero
} else {
self.scrollView.contentInset = UIEdgeInsets(top: 0,
left: 0,
bottom: keyboardViewEndFrame.height,
right: 0)
}
self.scrollView.scrollIndicatorInsets = self.scrollView.contentInset
},
completion: nil)
}
deinit {
let notificationCenter = NotificationCenter.default
notificationCenter.removeObserver(self)
}
}
Its usage is like this
create a reference to the keyboard manager
private var keyboardManager: KeyboardManager!
and assign the keyboard manager class like below in viewDidLoad where self.scrollView is the scrollView that you are working with
self.keyboardManager = KeyboardManager(scrollView: self.scrollView)
This should take care of the issue. If that does not work, probably a sample project might help to take a deep dive into that.

swift mac osx NSButton is not responding until a long press

I am having a weird issue with a button. So I have a NSViewController with many subviews in it. When I click a button, a new NSView with click gestures and buttons is added on top. But I can't press any of them, they don't respond unless a click for 2 seconds and then release. I've tried disabling the gestures of the holder but it didn't work. Any suggestions?
Well, some of the rest of us do. In my case, it's for buttons on a view in a sheet, so "many subviews" isn't likely it. My view controller for the sheet is about 100 lines. Still debugging...
At present the VC is as follows. The snp.makeConstraints calls are for SnapKit (from GitHub)
#objc
class ThreadEditSheetViewController: NSViewController {
/// The container for the graphics view
#IBOutlet var sheetView: NSView!
/// The information packet initialized by the invoking view controller
var info: ThreadEditInfo!
/// API
override func viewDidLoad() {
super.viewDidLoad()
}
/// API
override func viewWillAppear() {
guard let gvc = (try? self.bundleLoader(id: "GraphicsViewController")) as? GraphicsViewController else {
fatalUserAlert(error: AppError.UIConstructionFailure, message: "Can't find GraphicsViewController for ThreadEditSheetViewController")}
let gv = gvc.view
self.view.addSubview(gv)
// Spaces in title text move it left to avoid visual overlap with scroll bar. Don't know how to do it with
// constraints given the scrolling view
let done = makeButton(gvc: gvc, title: "done ", action: #selector(doneEditing(_:)))
done.snp.makeConstraints{ (make) in
make.top.equalTo(gv).offset(-5)
make.right.equalTo(gv).offset(-5)
}
let cancel = makeButton(gvc: gvc, title: "cancel", action: #selector(cancelEditing(_:)))
cancel.snp.makeConstraints{ (make) in
make.top.equalTo(gv).offset(-5)
make.left.equalTo(gv).offset(5)
}
self.view.becomeFirstResponder()
super.viewWillAppear()
return
}
func makeButton(gvc: NSViewController, title: String, action: Selector) -> NSButton {
let button = NSButton(title: title, target: self, action: action)
let gv = gvc.view
gv.addSubview(button)
button.backgroundColor = .clear
button.setButtonType(.momentaryChange)
button.isTransparent = true
return button
}
#objc
func doneEditing(_ sender: Any) {
self.dismissViewController(self)
}
#objc
func cancelEditing(_ sender: Any) {
self.dismissViewController(self)
}
}

How to add a tap gesture to multiple UIViewControllers

I'd like to print a message when an user taps twice on the remote of the Apple TV. I got this to work inside a single UIViewController, but I would like to reuse my code so that this can work in multiple views.
The code 'works' because the app runs without any problems. But the message is never displayed in the console. I'm using Swift 3 with the latest Xcode 8.3.3. What could be the problem?
The code of a UIViewController:
override func viewDidLoad() {
super.viewDidLoad()
_ = TapHandler(controller: self)
}
The code of the TapHandler class
class TapHandler {
private var view : UIView?
required init(controller : UIViewController) {
self.view = controller.view
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.message))
tapGesture.numberOfTapsRequired = 2
self.view!.addGestureRecognizer(tapGesture)
self.view!.isUserInteractionEnabled = true
}
#objc func message() {
print("Hey there!")
}
}
Your TapHandler just getting released. Try This:
var tapHandler:TapHandler? = nil
override func viewDidLoad() {
super.viewDidLoad()
tapHandler = TapHandler(controller: self)
}
I have tested the code and is working.

How to use custom UIAlertview in multiple ViewControllers?

I am looking for a way to call a custom alert view from multiple view controllers. So far I have made several different attempts without success.
I created an alert view with an interface builder that works fine on one view controller but not the other.
I then tried creating the alert view programmatically thinking it may have something to do with the outlets not being connected on the other view controller. This one also worked on one view controller and not the other.
I made a separate swift file and made a public function and the same result. With this last method, I am able to successfully re-use a regular UIAlertController on multiple view controllers but that is not exactly what I am looking for.
With the first two methods, I do not get any compiling errors. The app runs fine and then crashes when I call the alert from another view controller.
Thanks in advance for any input!
EDIT:
This example works when I put it in another swift file.
public func showSimpleAlert(title: String, message: String?, presentingController: UIViewController) {
if IS_OS_8_OR_LATER() {
let controller = UIAlertController(title: title, message: message, preferredStyle: .Alert)
controller.addAction(UIAlertAction(title: "OK", style: .Cancel, handler: { (action) -> Void in
}))
presentingController.presentViewController(controller, animated: true, completion: nil)
} else {
let alert = UIAlertView(title: title, message: message, delegate: nil, cancelButtonTitle: "OK")
alert.show()
}
}
This is the one I want to work on.
public func showAlert(oMsg: String, oTitle:String) {
alertView.backgroundColor = UIColor.whiteColor()
alertView.layer.cornerRadius = 25
alertTitleLabel.text = oTitle as String
alertTitleLabel.font = UIFont(name: "Open-Sans-Bold", size: 20)
alertTitleLabel.textColor = UIColor.blackColor()
alertTitleLabel.textAlignment = .Center
alertTitleLabel.numberOfLines = 1
alertTitleLabel.frame = CGRectMake(25, 60, 264, 112)
alertLabel.text = oMsg as String
alertLabel.font = UIFont(name: "Open-Sans", size: 20)
alertLabel.textColor = UIColor.blackColor()
alertLabel.textAlignment = .Center
alertLabel.numberOfLines = 4
alertLabel.frame = CGRectMake(25, 130, 264, 112)
okButton.setTitle("OK", forState: .Normal)
okButton.setTitleColor(UIColor.blueColor(), forState: .Normal)
okButton.frame = CGRectMake(60, 230, 197, 75)
okButton.addTarget(UIViewController.self, action:#selector(LoginViewController.buttonAction(_:)), forControlEvents: .TouchUpInside)
}
I will give the answer for a simple custom alertview which is basically a modified uiviewcontroller. you can use a uiviewcontroller as a uialertviewcontroller as follow.
Simple AlertView::
The AlertVC:
import UIKit
class ErrorAlert: UIViewController {
var titlenote:String = ""
var message:String = ""
#IBOutlet weak var cancelBtn: UIButton!
#IBOutlet weak var messageHolder: UILabel!
#IBOutlet weak var imageHolder: UIImageView!
#IBOutlet weak var titleHolder: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.black.withAlphaComponent(0.7)
// Do any additional setup after loading the view.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.messageHolder.text = self.message
self.titleHolder.text = self.titlenote
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func dismiss(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
}
}
This viewcontroller can be reuse in any vc and any number of times.
Useage Example::
let alertController = self.storyboard?.instantiateViewController(withIdentifier: "erroralert") as! ErrorAlert
alertController.titlenote = "Invalid login"
alertController.message = "Invalid facebook account."
alertController.providesPresentationContextTransitionStyle = true
alertController.definesPresentationContext = true
alertController.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
alertController.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
self.present(alertController, animated: true, completion: nil)
I have made the background of the alertviewvc semitransparent by setting the alpha value.
Actual Display ::
You can make more complex alertview by this method but for reusability you have apply some logic as the button actions will be different for different viewcontroller. Example -- Sometime you can use the alertview for logout alert or sometime for submitting a form .So in both cases the action will be different so for reusability you have to write extra logic.
Another alertView::
I hope my answer will help you.:)