I got a iPad-designed app using a SplitViewController showing two views, one with a contacts list and another with details of this contact. The SplitView works well on iPad but has some problems on iPhones.
There is a UIControl that take all the size of the Master View, that check if there is any .touchDown interaction by the user and some methods called to enable or disable this UIControl depending if we are on editing contact mode or not allowing user to interact with the screen or not :
private var disableInteractionClosure: (()->())?
private lazy var interactionOverlay: UIControl = {
let c = UIControl()
c.autoresizingMask = [.FlexibleHeight, .FlexibleWidth]
c.addTarget(self, action: "interactionOverlayAction", forControlEvents: .TouchDown)
return c
}()
func disableInteractionWhileEditingWithClosure(callback: ()->()) {
if disableInteractionClosure == nil {
disableInteractionClosure = callback
/* display control overlay */
interactionOverlay.frame = navigationController!.view.bounds
navigationController!.view.addSubview(interactionOverlay)
}
}
func interactionOverlayAction() {
disableInteractionClosure!()
}
func enableInteraction() {
disableInteractionClosure = nil
interactionOverlay.removeFromSuperview()
}
Basically the UIControl is used to block user from switching between contact while user is editing another contact/ creating a new one by blocking interaction with the contact list and if changes have been made on the editing/creating view it fires a method that shows a pop up saying "modifications have been made do you want to continue without saving ? cancel or continue " :
func cancel() {
self.view.endEditing(true)
let closure: ()->() = {
self.layoutView.willResign()
self.delegate?.tabDetailsControllerDidCancel(self)
}
if layoutView.hasChanges() {
MKAlertViewController().instantaneousAlert(title: "Modification apportées", message: "Êtes-vous sur de vouloir continuer sans sauvegarder les modifications ?", confirmButtonTitle: "Oui", cancelButtonTitle: "Annuler", confirmed: { () -> Void in
closure()
}, canceled: { () -> Void in
})
} else {
closure()
}
}
It works fine on iPad because the UIControl is only above the Master View and is enabled when in editing mode on the Detail View (iPad 3D Debugging view), so the pop up shows only when manually cancelling the editing/creating or when trying to change contact while editing/creating, but as the splitView don't work the same on iPads and iPhones and it appears that on iPhone the Master View is placed above the Detail View, the UIControl is also above (iPhone 3D Debugging view), and it causes to block interactions on all the screen and wherever I click the cancel pop-up is shown.
Can you guys explain me a way to enable/show this UIControl only when the MasterView is showing and not everywhere. Thanks.
I ended up using the viewWillDisappear on the detail view :
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
if self.isMovingFromParentViewController() || self.isBeingDismissed() {
if editingMode {
shared.contactsListController.disableInteractionWhileEditingWithClosure({ (_) in
self.tabDetailsController.cancel()
})
shared.contactsListController.disableToolbar()
} else {
shared.contactsListController.enableInteraction()
shared.contactsListController.enableToolbar()
}
self.navigationController?.toolbar.alpha = 1
}
}
and modifying the disableInteractionWhileEditingWithClosure method on the master view:
func disableInteractionWhileEditingWithClosure(callback: ()->()) {
if disableInteractionClosure == nil {
disableInteractionClosure = callback
/* display control overlay */
interactionOverlay.frame = navigationController!.view.bounds
view.addSubview(interactionOverlay) // this line
}
}
and it works ! :)
Related
I want to implement a shake to undo on my iPhone App, in Swift.
For now it works, but it didn't display an Alert asking to validate the undo gesture (with buttons "Cancel" and "Undo"). I can add this alert in the right place myself, but I'm not certain I should. Some articles make me think that the alert should appear automatically, in the undo/redo process, so there's something I missed, perhaps...
Here are the relevant bits of code
In the viewController
override func becomeFirstResponder() -> Bool {
return true
}
and
override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
if motion == .motionShake {
snowflake.undoLastAction()
}
}
and in my snowflake class, the action is inserting or modifying point in an array, so I store the value before the change in oldPathPoints, then
// Action du undo
undoManager?.registerUndo(withTarget: self, handler: { (targetSelf) in
snowflake.pathPoints = oldPathPoints
})
and finally the undo method
func undoLastAction() {
undoManager?.undo()
createPath()
}
So,
I succeeded to do it (with the help of a 5-years Book from Matt Neuburg!)
So, there's nothing to make in the viewController, I deleted becomeFirstResponder and motionEnded.
All is in the SnowFlake class: I added
let undoer = UndoManager()
override var undoManager : UndoManager? {
return self.undoer
}
override var canBecomeFirstResponder: Bool { return true }
So I communicate the undoManager I use and the class (a view here) can become firstResponder
after the manipulation of the view that can be undone (in my case in touchesEnded)
, I call
self.becomeFirstResponder()
I deleted the undoLastAction function (useless now), and
changed the undo action to a fonction
changePathUndoable(newPathPoints: finalPathPoints)
the function is like that
func changePathUndoable(newPathPoints: [CGPoint]) {
let oldPathPoints = Snowflake.sharedInstance.pathPoints
Snowflake.sharedInstance.pathPoints = newPathPoints
// Action du undo
undoer.setActionName("cutting")
undoer.registerUndo(withTarget: self, handler: { (targetSelf) in
targetSelf.changePathUndoable(newPathPoints: oldPathPoints)
})
Snowflake.sharedInstance.createPath()
self.setNeedsDisplay()
}
I had to create the function changePathUndoable instead of calling undoer.registerUndo directly just for having the redo action to work (I didn't test redo before).
I think that all the steps are necessary to do what I wanted: make the view receive the shake event, displays this dialog
and be able to undo/redo the modifications.
I've a subclass of UIPageControl, and I'd like to observe the changes in currentPage.
Unfortunately, my currentPage's didSet isn't called when currentPage changes from taps on MyPageControl.
class MyPageControl: UIPageControl {
override var currentPage: Int {
didSet {
// not called when `currentPage` is changed from a tap
updateSomething(for: currentPage)
}
}
}
And the change can't be observed using KVO either:
class MyPageControl: UIPageControl {
var observation: NSKeyValueObservation?
override func awakeFromNib() {
super.awakeFromNib()
observation = observe(\.currentPage) { (_, _) in
// not called when `currentPage` is changed from a tap
updateSomething(for: self.currentPage)
}
}
}
From Apple Doc : Apple Doc
When a user taps a page control to move to the next or previous page, the control sends the valueChanged event for handling by the delegate. The delegate can then evaluate the currentPage property to determine the page to display. The page control advances only one page in either direction. The currently viewed page is indicated by a white dot. Depending on the device, a certain number of dots are displayed on the screen before they are clipped.
Code:
pageControl.addTarget(self, action:#selector(pageControlTapHandler(sender:)), for: .touchUpInside)
//or
pageControl.addTarget(self, action:#selector(pageControlTapHandler(sender:)), for: .valueChanged)
...
func pageControlTapHandler(sender:UIPageControl) {
print("currentPage:", sender.currentPage) //currentPage: 1
}
You may detect the taps with sendAction(_:to:for:) and read the value of currentPage at that moment.
class MyPageControl: UIPageControl {
override func sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {
super.sendAction(action, to: target, for: event)
updateSomething(for: currentPage)
}
}
I have created a custom view that is to be used as a radio button with images and text. I need to be able to load the saved selection when the controller loads. I set my listeners this way:
for button in genderButtons {
button.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(genderTapped(_:))))
}
#objc private func genderTapped(_ sender: UITapGestureRecognizer) {
for button in genderButtons {
button.select(sender.view! == button) // Toggles the button to display selected/deslected state.
...
}
}
The problem is that I can't find a way to tell the view to select. I tried making the gesture recognizer and object, but it doesn't have any methods I can use to trigger it. The 'buttons' aren't actually buttons, they're views, so I can't send an action event.
How can I select the correct button with code?
Just call genderTapped directly, handing it the gesture recognizer already attached to the desired "button".
For example, if thisGenderButton is the one you want to "tap", say:
if let tap = thisGenderButton.gestureRecognizers?[0] as? UITapGestureRecognizer {
genderTapped(tap)
}
You can add this method in your customView like this,
Class CustomView: UIView {
public func select(_ value: Bool) {
self.backgroundColor = value ? .green: .red
}
}
and then in below method you can call select for the tapped view.
#objc private func genderTapped(_ sender: UITapGestureRecognizer) {
(sender.view as? CustomView)?.select(true)
}
I have a view which I set up as input accessory view for view controller the following way:
#IBOutlet private weak var bottomPane: UIView!
override func canBecomeFirstResponder() -> Bool {
return true
}
override var inputAccessoryView: UIView {
return bottomPane
}
Everything works just fine until I try to view YouTube video in fullscreen mode (video is loaded in UIWebView). When video enters fullscreen mode, keyboard and my input accessory view disappear (which is normal, I guess), but when I exit fullscreen mode, they do not appear. If I keep the reference to bottomPane weak, it becomes nil and application crashes, if I change it to strong, input accessory view remains hidden until the keyboard appears next time.
Can anybody explain what's going on and how to fix this?
Here's what's going on.
When user interacts with UIWebView, it becomes first responder and inputAccessoryView provided by view controller disappears (no idea why behavior in this case is different from, say, UITextField). Subclassing UIWebView and overriding inputAccessoryView property does not work (never gets called). So I block interaction with UIWebView until user loads video.
private func displayVideo(URL: String) {
if let video = Video(videoURL: URL) {
// load video in webView
webView.userInteractionEnabled = true
} else {
webView.userInteractionEnabled = false
}
}
When user loads video, the only way to detect that user has entered/exited fullscreen mode is to listen to UIWindowDidBecomeKeyNotification and UIWindowDidResignKeyNotification and detect when our window loses/gains key status:
//in view controller:
private func windowDidBecomeKey(notification: NSNotification!) {
let isCurrentWindow = (notification.object as! UIWindow) == view.window
if isCurrentWindow {
// this restores our inputAccessoryView
becomeFirstResponder()
}
}
private func windowDidResignKey(notification: NSNotification!) {
let isCurrentWindow = (notification.object as! UIWindow) == view.window
if isCurrentWindow {
// this hides our inputAccessoryView so that it does not obscure video
resignFirstResponder()
}
}
And, of course, since inputAccessoryView can be removed at some point, we should recreate it if needed:
//in view controller:
override var inputAccessoryView: UIView {
if view == nil {
// load view here
}
return view
}
I'm new to swift. My app uses photos people upload to the web and showing the photos in a table view.
It is reloading whenever some user uploads a new photo.
I have a UITextField that when you press it the keyboard goes up. My problem is that it goes down whenever reloadView is happening (when a new photo arrives)
What i'm trying to do is to check if the UITextField is first recogniser and if so I want to wait with the reload until it not first recogniser.
func refreshView()
{
dispatch_async(dispatch_get_main_queue()) { () -> Void in
while (self.dataSource.writeSomethingTextLabel.isFirstResponder()) {
//need to wait somehow for notification that it is not first responder anymore
}
self.tableView.reloadData()
}
}
So with this code the keyboard is not going down of course but the wile loop runs and everything is stuck. my question is how can I wait until the user finishes using the text label (first responder is false).
thanks
You can conditionally reload tableView on two places: when new data arrives and when textField ends editing:
#class YourViewController: UIViewController, UITextFieldDelegate {
var needsToReloadTableView = false;
func reloadTableViewIfNeeded() {
if needsToReloadTableView {
self.tableView.reloadData()
needsToReloadTableView = false
}
}
func gotNewData() {
needsToReloadTableView = true
if !self.textField.isFirstResponder {
reloadTableViewIfNeeded()
}
}
func textFieldDidEndEditing(textField:UITextField) {
reloadTableViewIfNeeded()
}
}
#IBAction func viewTapped(sender : AnyObject) {
totalTextField.resignFirstResponder()
}