UITextView with other elements (similar to Evernote) iOS - ios

I'm trying to implement a view very similar to Evernote's screen in which you add a New Note.
It seems like a UITableView embedded in a NavigationController. This tableview contains static cells (2 or 3) with the bottom one being a UITextView in which you add the content of the note, but when you scroll on the textView, the other cells that contain a textField and another control.
How can this be achieved? I know that Apple doesn't recommend a TextView inside a ScrollView, and doing it with table view it gets a bit weird with all the scrolling from the table and text view.
Here are some examples:
Any suggestions?
Thank you!

Firstly, They disabled text view scrolling and set its size to about screen size. Secondly, once text view's text is out of frame, expand it(calculate its size again).

So I found my problem, when I was setting the constraints for the content view (view inside scrollview) I set an Equal value for its height. To fix it I just made that relationship to Greater or Equal than... it now expands.
The other problem now is that when showing the keyboard it is not scrolling to the text I tap to. (The insets are properly setup though)
// MARK: Notififations from the Keyboard
func didShowKeyboard (notification: NSNotification) {
if momentTextView.isFirstResponder() {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.size.height, right: 0)
scrollView.scrollIndicatorInsets = scrollView.contentInset
let caretPosition = momentTextView.caretRectForPosition(momentTextView.selectedTextRange!.start)
let newHeight = caretPosition.height * 1.5
let newCaretPosition = CGRect(x: caretPosition.origin.x, y: caretPosition.origin.y, width: caretPosition.width, height: newHeight)
scrollView.scrollRectToVisible(newCaretPosition, animated: true)
}
}
}
func willHideKeyboard (notification: NSNotification) {
if momentTextView.isFirstResponder() {
scrollView.contentInset = UIEdgeInsetsZero
scrollView.scrollIndicatorInsets = UIEdgeInsetsZero
}
}

Related

Programmatically scroll uiscrollview to bottom as uilabel grows in height

I am using SFSpeechRecognizer to transcribe audio to text. As the audio is transcribed, I am printing it out as results come out to a uilabel, which is a subview of a uiscrollview. The uilabel is pinned to the top and bottom of the uiscrollview.
My problem is programmatically scrolling the uiscrollview to the bottom as the uilabel grows in height from the transcription.
I tried solutions such as this but it does not work. Any guidance would be much appreciated.
Code I've tried:
SFSpeechRecognizer().recognitionTask(with: request) { [unowned self] (result, error) in
guard let result = result else {
DispatchQueue.main.async {
self.transcriptionLabel.text = "We're not able to transcribe."
}
return
}
DispatchQueue.main.async {
self.transcriptionLabel.text = result.bestTranscription.formattedString
if self.transcriptionScrollView.contentSize.height - self.transcriptionScrollView.bounds.size.height > 0 {
let bottomOffset = CGPoint(x: 0, y: self.transcriptionScrollView.contentSize.height - self.transcriptionScrollView.bounds.height + self.transcriptionScrollView.contentInset.bottom)
self.transcriptionScrollView.setContentOffset(bottomOffset, animated: true)
}
self.view.layoutIfNeeded()
}
}
Again, the first thing you need to do is layout your subviews after updating the text so your computations are based on the new text.
Rather than jumping through a lot of hoops and trying to compute the contentOffset I would suggest using scrollToRectVisible(_:animated:).
view.layoutIfNeeded()
let rect = CGRect(
x: 0,
y: transcriptionLabel.frame.maxY - 1,
width: 1,
height: 1)
transcriptionScrollView.scrollRectToVisible(rect, animated: true)
That should scroll you to the bottom of the label, assuming that your label is a subview of your scrollView. If not, you will need to convert the label's frame to the scrollView coordinate space.
Remember to call view.layoutIfNeeded() after updating the text in your label and before scrolling.

How can I keep UITableView in view when keyboard appears?

I'm trying to create a page in an app that's your standard style messaging screen. I'm having trouble getting everything to position correctly when the keyboard slides into view. I'll post screenshots (sadly not inline), but here is my structure:
VIEWCONTROLLER
|-View
|-Scroll View
|-Content View
|-TextField
|-TableView (messages)
Everything is showing up as I would like it to when first loaded: If there aren't enough messages to fill the screen, the messages start at the top followed by a gap, and the text field is pinned to the bottom. Nothing scrolls. If there are a lot of messages, I am successfully scrolling the table to the last row and the textfield is pinned to the bottom of the screen still.
When the textfield is activated however, and there aren't a lot of messages, the gap between the table and the textfield remains and the messages are pushed out of view to the top.
I am trying to get the gap to shrink so the messages stay. This is standard in other messaging apps, but I cannot figure out how to do it
Initial view
Textfield activated, keyboard appears
Scrolling to display messages hides the textfield
UI Layout and constraints
Lastly, here is the code I have for keyboardWillShow. You'll notice some comments of things I have tried unsuccessfully.
func keyboardWillShow(notification:NSNotification) {
var userInfo = notification.userInfo!
let keyboardFrame = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size
let contentInsets: UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardFrame!.height, 0.0)
self.scrollView.contentInset = contentInsets
self.scrollView.scrollIndicatorInsets = contentInsets
// scrollViewBottomConstraint.constant = keyboardFrame!.height - bottomLayoutGuide.length
// contentViewHeightConstraint.constant = -keyboardFrame!.height
// self.notificationReplyTable.frame.size.height -= keyboardFrame!.height
var aRect: CGRect = self.view.frame
aRect.size.height -= keyboardFrame!.height
if let activeField = self.activeField {
if(!aRect.contains(activeField.frame.origin)) {
self.scrollView.scrollRectToVisible(activeField.frame, animated: true)
}
}
}
I feel like the piece I'm missing is pretty small, but just don't know enough Swift 3 to nail this. Thank you for your help!
Edit: the problem is similar to this question with no accepted answer.
A way to this is to set up vertical autolayout constraints like this (but you will need a reference to the actual bottomMargin constraint to be able to modify it) :
"V:|[scrollView][textField]-(bottomMargin)-|"
The first time you arrive on the screen, bottomMargin is set to 0.
Then when keyboardWillShow is called, get the keyboard frame (cf How to get height of Keyboard?)
func keyboardWillShow(_ notification: Notification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
}
}
And animate the constraint bottomMargin to get the height of the keyboard (the duration is 0.3 after some tests, but you can adjust it) :
bottomConstraint.constant = keyboardHeight
UIView.animate(withDuration: 0.3, delay: 0, options: nil, animations: {
self.view.layoutIfNeeded()
}
That means that every time the keyboard will appear, an animation will move up the text field, hence the scroll view height will be smaller and everything will fit in the screen.
!! Don't forget to test it on landscape mode if you support it, and on iPad too!!
Finally, handle the case when the keyboard will disappear in the keyboardWillHide and set bottomMargin back to 0 :
func keyboardWillHide(_ notification: Notification) {
bottomConstraint.constant = 0
UIView.animate(withDuration: 0.3, delay: 0, options: nil, animations: {
self.view.layoutIfNeeded()
}
}

safeAreaInsets in UIView is 0 on an iPhone X

I am updating my app to adapt it for iPhone X. All views work fine by now except one. I have a view controller that presents a custom UIView that covers the whole screen. Before I was using UIScreen.main.bounds to find out the size of the view before all layout was done (I need it for putting the correct itemSize for a collectionView). I thought that now I could do something like
UIScreen.main.bounds.size.height - safeAreaInsets.bottom to get the right usable size. The problem is, safeAreaInsets returns (0,0,0,0) trying on an iPhone X (Simulator). Any ideas? In other views, I get the right numbers for safeAreaInsets.
Thank you!
I recently had a similar problem where the safe area insets are returning (0, 0, 0, 0) as soon as viewDidLoad is triggered. It seems that they are set fractionally later than the rest of the view loading.
I got round it by overriding viewSafeAreaInsetsDidChange and doing my layout in that instead:
override func viewSafeAreaInsetsDidChange() {
// ... your layout code here
}
I already figure out the solution: I was doing all the implementation in the init of the view. safeAreaInsets has the correct size in layoutSubviews()
I've run into this issue too trying to move up views to make way for the keyboard on the iPhone X. The safeAreaInsets of the main view are always 0, even though I know the subviews have been laid out at this point as the screen has been drawn. A work around I found, as and mentioned above, is to get the keyWindow and check its safe area insets instead.
Obj-C:
CGFloat bottomInset = [UIApplication sharedApplication].keyWindow.safeAreaInsets.bottom;
Swift:
let bottomInset = UIApplication.shared.keyWindow?.safeAreaInsets.bottom
You can then use this value to adjust constraints or view frames as required.
I have a view which is a subview inside another view.
I found that I can't get safeAreaInsets correctly, it always return 0, in that view on iPhoneX even if I put it in layoutSubviews.
The final solution is I use following UIScreen extension to detect safeAreaInsets which can work like a charm.
extension UIScreen {
func widthOfSafeArea() -> CGFloat {
guard let rootView = UIApplication.shared.keyWindow else { return 0 }
if #available(iOS 11.0, *) {
let leftInset = rootView.safeAreaInsets.left
let rightInset = rootView.safeAreaInsets.right
return rootView.bounds.width - leftInset - rightInset
} else {
return rootView.bounds.width
}
}
func heightOfSafeArea() -> CGFloat {
guard let rootView = UIApplication.shared.keyWindow else { return 0 }
if #available(iOS 11.0, *) {
let topInset = rootView.safeAreaInsets.top
let bottomInset = rootView.safeAreaInsets.bottom
return rootView.bounds.height - topInset - bottomInset
} else {
return rootView.bounds.height
}
}
}
I try to use "self.view.safeAreaInset" in a view controller. First, it is a NSInsetZero when I use it in the controller's life cycle method "viewDidLoad", then I search it from the net and get the right answer, the log is like:
ViewController loadView() SafeAreaInsets :UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
ViewController viewDidLoad() SafeAreaInsets :UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
ViewController viewWillAppear() SafeAreaInsets :UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
ViewController viewDidLayoutSubviews() SafeAreaInsets :UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
ViewController viewDidAppear() SafeAreaInsets :UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
so you can choice the right method that you need the safeAreaInset and use it!
Swift iOS 11,12,13+
var insets : UIEdgeInsets = .zero
override func viewDidLoad() {
super.viewDidLoad()
insets = UIApplication.shared.delegate?.window??.safeAreaInsets ?? .zero
//Or you can use this
insets = self.view.safeAreaInsets
}
In my case I was adding a UICollectionView inside viewDidLoad()
collectionView = UICollectionView(frame: view.safeAreaLayoutGuide.layoutFrame, collectionViewLayout: createCompositionalLayout())
Unfortunately at this stage safeAreaLayoutGuide is still zero.
I solved it by adding:
override func viewDidLayoutSubviews() {
collectionView.frame = view.safeAreaLayoutGuide.layoutFrame
}
the viewDidAppear(_:) method of the container view controller that extends the safe area of its embedded child view controller to account for the views in .
Make your modifications in this method because the safe area insets for a view are not accurate until the view is added to a view hierarchy.
override func viewDidAppear(_ animated: Bool) {
if (#available(iOS 11, *)) {
var newSafeArea = view.safeAreaInsets
// Adjust the safe area to accommodate
// the width of the side view.
if let sideViewWidth = sideView?.bounds.size.width {
newSafeArea.right += sideViewWidth
}
// Adjust the safe area to accommodate
// the height of the bottom view.
if let bottomViewHeight = bottomView?.bounds.size.height {
newSafeArea.bottom += bottomViewHeight
}
// Adjust the safe area insets of the
// embedded child view controller.
let child = self.childViewControllers[0]
child.additionalSafeAreaInsets = newSafeArea
}
}
I've come across the same problem. In my case the view I'm inserting would be sized correctly after calling view.layoutIfNeeded(). The view.safeAreaInsets was set after this, but only the top value was correct. The bottom value was still 0 (this on an iPhone X).
While trying to figure out at what point the safeAreaInsets are set correctly, I've added a breakpoint on the view's safeAreaInsetsDidChange() method. This was being called multiple times, but only when I saw CALayer.layoutSublayers() in the backtrace the value had been set correctly.
So I've replaced view.layoutIfNeeded() by the CALayer's counterpart view.layer.layoutIfNeeded(), which resulted in the safeAreaInsets to be set correctly right away, thus solving my problem.
TL;DR
Replace
view.layoutIfNeeded()
by
view.layer.layoutIfNeeded()
[UIApplication sharedApplication].keyWindow.safeAreaInsets return none zero
Just try self.view.safeAreaInsets instead of UIApplication.shared.keyWindow?.safeAreaInsets
Safe area insets seems to not fill on iOS 11.x.x devices when requested via application keyWindow.
View layout is never guaranteed until layoutSubviews or viewDidLayoutSubviews. Never rely on sizes before these lifecycle methods. You will get inconsistent results if you do.
To calculate safe area safeAreaInsets, try to obtain it in viewWIllAppear(), as in didLoad() the view have not been formed.
You will have the correct inset in willAppear!
In case you cannot subclass, you can use this UIView extension.
It gives you an API like this:
view.onSafeAreaInsetsDidChange = { [unowned self] in
self.updateSomeLayout()
}
The extension adds an onSafeAreaInsetsDidChange property using object association. Then swizzles the UIView.safeAreaInsetsDidChange() method to call the closure (if any).
extension UIView {
typealias Action = () -> Void
var onSafeAreaInsetsDidChange: Action? {
get {
associatedObject(for: "onSafeAreaInsetsDidChange") as? Action
}
set {
Self.swizzleSafeAreaInsetsDidChangeIfNeeded()
set(associatedObject: newValue, for: "onSafeAreaInsetsDidChange")
}
}
static var swizzled = false
static func swizzleSafeAreaInsetsDidChangeIfNeeded() {
guard swizzled == false else { return }
swizzle(
method: "safeAreaInsetsDidChange",
originalSelector: #selector(originalSafeAreaInsetsDidChange),
swizzledSelector: #selector(swizzledSafeAreaInsetsDidChange),
for: Self.self
)
swizzled = true
}
#objc func originalSafeAreaInsetsDidChange() {
// Original implementaion will be copied here.
}
#objc func swizzledSafeAreaInsetsDidChange() {
originalSafeAreaInsetsDidChange()
onSafeAreaInsetsDidChange?()
}
}
It uses some helpers (see NSObject+Extensions.swift and NSObject+Swizzle.swift), but you don't really need it if you use sizzling and object association APIs directly.

Maintain visibility of focused UITextView in a UITableView when showing Keyboard

I am building a form dynamically that has various types of UI elements for each UITableViewCell.
There is one UITableViewCell that contains a UITextView and I want to be able to maintain visibility when showing the keyboard. I have looked at the other similar questions, but have been unable to find a solution.
I wrote the Swift version of what is recommended by: Apple's Managing Keyboard
It does not work for two reasons.
1.) The Keyboard notification is fired before the TextViewWillBeginEditing.
2.) The frame of the UITextView is in relation to the superview which is the UITableViewCell, so the check is wrong.
Here is my current code:
func adjustTableViewForKeyboard(notification: NSNotification) {
let userInfo = notification.userInfo!
let keyboardScreenEndFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue()
let keyboardViewEndFrame = view.convertRect(keyboardScreenEndFrame, fromView: view.window)
if notification.name == UIKeyboardWillHideNotification {
self.tableView.contentInset = UIEdgeInsetsZero
} else {
self.tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardViewEndFrame.height, right: 0)
rect = self.view.frame;
rect!.size.height -= keyboardViewEndFrame.height
}
self.tableView.scrollIndicatorInsets = self.tableView.contentInset
}
func textViewDidBeginEditing(textView: UITextView) {
self.activeTextView = textView;
if(activeTextView != nil){
// This check does NOT work due to the TextView's superview is the cell.
if(!CGRectContainsPoint(rect!, (activeTextView?.frame.origin)!)){
self.tableView.scrollRectToVisible((activeTextView?.frame)!, animated: true)
}
}
}
This works in terms of being able to scroll to all cells, but I also want to make sure the UITextView is not hidden by keyboard.
You may be able to use the responder chain to solve this. In your notification handler, try calling isFirstResponder() on your text view; if it returns true, then you can call the table view-scrolling code from there.
I had a similar issue with a (albeit static cell) form I was building, I was able to use the scrollToRowAtIndexPath method to update the tableview's row positioning to keep my text view on screen.
func textViewDidBeginEditing(textView: UITextView) {
if textView == notesTextView {
tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 3), atScrollPosition: .Top, animated: true)
}
}
It does require knowledge of the index path section/row and the text view reference in case of multiples, but might be of use?
scrollRectToVisible should also work, but sounds like you have to convert the textview's frame to coordinate system of the scrollview using convertRect:toView or similar first.
Hope this helps - Cheers.

How to remove empty space above a UITableView?

So I am using a UITableView to display information about different films.
At the top of the VC, I have a UIImage which sits inside of a UIView. And then my table sits underneath. The table currently sits right up against the bottom of the image (which is what I want), see below:
The Issue
I followed a tutorial to add a simple effect, so when the user pulls down on the tableView, the image enlarges. You can see what I mean by seeing the tutorial here: See here
This all worked wonderful and gave my the effect I wanted, however, it's now added an empty space below the image, see the image below:
Everything still works fine, and the effect works as expected, but this space is now there - which I really don't want.
The settings in the storyboard for this VC are set as followed:
The code I added to make the effect is as follows:
private let KTableHeaderHeight: CGFloat = 160.0 // which is the height of my UIImage
var headerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
headerView = tableView.tableHeaderView
tableView.tableHeaderView = nil
tableView.addSubview(headerView)
tableView.contentInset = UIEdgeInsets(top: KTableHeaderHeight, left: 0, bottom: 0, right: 0)
tableView.contentOffset = CGPoint(x: 0, y: -KTableHeaderHeight)
updateHeaderView()
}
And then:
func updateHeaderView() {
var headerRect = CGRect(x: 0, y: -KTableHeaderHeight, width: tableView.bounds.width, height: KTableHeaderHeight)
if tableView.contentOffset.y < -KTableHeaderHeight {
headerRect.origin.y = tableView.contentOffset.y
headerRect.size.height = -tableView.contentOffset.y
}
headerView.frame = headerRect
}
override func scrollViewDidScroll(scrollView: UIScrollView) {
updateHeaderView()
}
If I comment out all the code I've added, it then looks fine again, so I'm guessing it is this code that's causing the space.
I'm really keen to understand why this gap has formed, and how I can remove it, still using the code added to make the image enlarging effect.
Update
My UIImageView layout Attributes:
Your issue is likely the private let KTableHeaderHeight: CGFloat = 160.0 line, which doesn't equal the height of the imageView in the header.
You need to find out the fixed height of the imageView after scaling, which you can get by multiplying the original image height by view width/original image width, and set the imageView height and private var KTableHeaderHeight: CGFloat to that value.
If your using a grouped UITableView then it makes a space for the group, not sure how to get rid of it. You might want to consider making it a plan UITableView and then create the sections headers that you want via the delegate
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
// create UIView here
}
However, after playing with the sample project, it seems like you can adjust your kTableHeightHeader variable in the top to adjust to hide the top section header.
I modified the sample project so you can see it here
Swift code:
tableView.tableFooterView = UIView()
Objective C code:
tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];

Resources