Skip to content

Commit

Permalink
Fixed scrollbars and reduced first responder juggling (#2327)
Browse files Browse the repository at this point in the history
* fixed scrollbars, less first responder juggling

* fixed memory leak
  • Loading branch information
Amzd authored Oct 19, 2024
1 parent bf1e2e7 commit 94c35df
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 53 deletions.
4 changes: 4 additions & 0 deletions deltachat-ios.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
30FDB70524D1C1000066C48D /* ChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FDB6F824D1C1000066C48D /* ChatViewController.swift */; };
30FDB71F24D8170E0066C48D /* TextMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FDB71E24D8170E0066C48D /* TextMessageCell.swift */; };
30FDB72124D838240066C48D /* BaseMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FDB72024D838240066C48D /* BaseMessageCell.swift */; };
5F153E9A2CC38BAA00871ABE /* GiveBackMyFirstResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F153E992CC38BAA00871ABE /* GiveBackMyFirstResponder.swift */; };
5F785F6E2CB9344F003FFFB9 /* ReusableCellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F785F6D2CB9344F003FFFB9 /* ReusableCellProtocol.swift */; };
7070FB9B2101ECBB000DC258 /* NewGroupController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7070FB9A2101ECBB000DC258 /* NewGroupController.swift */; };
7092474120B3869500AF8799 /* ContactDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7092474020B3869500AF8799 /* ContactDetailViewController.swift */; };
Expand Down Expand Up @@ -433,6 +434,7 @@
30FDB6F824D1C1000066C48D /* ChatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatViewController.swift; sourceTree = "<group>"; };
30FDB71E24D8170E0066C48D /* TextMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageCell.swift; sourceTree = "<group>"; };
30FDB72024D838240066C48D /* BaseMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseMessageCell.swift; sourceTree = "<group>"; };
5F153E992CC38BAA00871ABE /* GiveBackMyFirstResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiveBackMyFirstResponder.swift; sourceTree = "<group>"; };
5F785F6D2CB9344F003FFFB9 /* ReusableCellProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableCellProtocol.swift; sourceTree = "<group>"; };
7070FB9A2101ECBB000DC258 /* NewGroupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewGroupController.swift; sourceTree = "<group>"; };
7092474020B3869500AF8799 /* ContactDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactDetailViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1025,6 +1027,7 @@
AEACE2E41FB32E1900DCDD78 /* Utils.swift */,
AE851AC4227C755A00ED86F0 /* Protocols.swift */,
3095A350237DD1F700AB07F7 /* MediaPicker.swift */,
5F153E992CC38BAA00871ABE /* GiveBackMyFirstResponder.swift */,
30AC265E237F1807002A943F /* AvatarHelper.swift */,
302B84C42396627F001C261F /* RelayHelper.swift */,
AE1988A423EB2FBA00B4CD5F /* Errors.swift */,
Expand Down Expand Up @@ -1630,6 +1633,7 @@
D8FB04002B4F0CF100A355F8 /* EmojiView.swift in Sources */,
3008CB7624F95B6D00E6A617 /* AudioController.swift in Sources */,
3080A035277DE30100E74565 /* String+Extensions.swift in Sources */,
5F153E9A2CC38BAA00871ABE /* GiveBackMyFirstResponder.swift in Sources */,
302B84CE2397F6CD001C261F /* URL+Extension.swift in Sources */,
7A9FB1441FB061E2001FEA36 /* AppDelegate.swift in Sources */,
30C7D5EE28F47E620078D24C /* MessageCounter.swift in Sources */,
Expand Down
89 changes: 44 additions & 45 deletions deltachat-ios/Chat/ChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import UIKit
import AVFoundation
import DcCore
import SDWebImage
import Combine

class ChatViewController: UITableViewController, UITableViewDropDelegate {
public let chatId: Int

private var dcContext: DcContext
private var messageIds: [Int] = []
private var isVisibleToUser: Bool = false
private var wasInputBarFirstResponder = false
private var reactionMessageId: Int?
private var contextMenuVisible = false

Expand Down Expand Up @@ -157,11 +157,18 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
get { customInputAccessoryView }
set { customInputAccessoryView = newValue }
}

/// Set this to false if you are doing something with the UI inside this view controller that
/// requires the inputAccessoryView to be hidden. Do not set this when navigating,
/// because UIKit should automatically return the firstResponder.
private var shouldBecomeFirstResponder: Bool = false
override var canBecomeFirstResponder: Bool {
if let presentedViewController {
if let presentedViewController, !presentedViewController.isBeingDismissed {
// Should not show inputAccessoryView when anything other than searchController is presented
return presentedViewController is UISearchController && shouldBecomeFirstResponder
} else if navigationController?.topViewController != self {
// Don't show inputAccessoryView when not top view controller
return false
} else {
return shouldBecomeFirstResponder
}
Expand Down Expand Up @@ -190,6 +197,12 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
return view
}()

private var _bag: [Any/*Cancellable*/] = []
@available(iOS 13.0, *) private var bag: [AnyCancellable] {
get { _bag.compactMap { $0 as? AnyCancellable } }
set { _bag = newValue }
}

init(dcContext: DcContext, chatId: Int, highlightedMsg: Int? = nil) {
self.dcContext = dcContext
self.chatId = chatId
Expand Down Expand Up @@ -233,6 +246,12 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
tableView.transform = CGAffineTransform(scaleX: 1, y: -1)
// Since the view is flipped, its safeArea will be flipped, luckily we can ignore it
tableView.contentInsetAdjustmentBehavior = .never
if #available(iOS 13.0, *) {
tableView.automaticallyAdjustsScrollIndicatorInsets = false
tableView.publisher(for: \.contentInset)
.assign(to: \.scrollIndicatorInsets, on: tableView)
.store(in: &bag)
}

navigationController?.setNavigationBarHidden(false, animated: false)

Expand All @@ -245,27 +264,27 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {

// Binding to the tableView will enable interactive dismissal
keyboardManager?.bind(to: tableView)
keyboardManager?.on(event: .willShow) { [weak self] notification in
guard let self else { return }
let globalTableViewFrame = self.tableView.convert(tableView.bounds, to: tableView.window)
keyboardManager?.on(event: .willShow) { [tableView = tableView!] notification in
// Using superview instead of window here because in iOS 13+ a modal can change
// the frame of the vc it is presented over which causes this calculation to be off.
let globalTableViewFrame = tableView.convert(tableView.bounds, to: tableView.superview)
let intersection = globalTableViewFrame.intersection(notification.endFrame)
let inset = intersection.height
// willShow is sometimes called when the keyboard is being hidden or when the kb was
// already shown due to interactive dismissal getting canceled.
guard self.tableView.contentInset.top != inset else { return }
guard tableView.contentInset.top != inset else { return }
UIView.animate(withDuration: notification.timeInterval, delay: 0, options: notification.animationOptions) {
self.tableView.contentInset.top = inset
if self.tableView.contentOffset.y < 30 {
tableView.contentInset.top = inset
if tableView.contentOffset.y < 30 {
// If user is less than 30 away from the bottom, we scroll
// the bottom of the content to the top of the keyboard.
self.tableView.contentOffset.y -= inset + self.tableView.contentOffset.y
tableView.contentOffset.y -= inset + tableView.contentOffset.y
}
}
}
keyboardManager?.on(event: .willHide) { [weak self] notification in
guard let self else { return }
keyboardManager?.on(event: .willHide) { [tableView, inputAccessoryView] notification in
UIView.animate(withDuration: notification.timeInterval, delay: 0, options: notification.animationOptions) {
self.tableView.contentInset.top = self.inputAccessoryView?.frame.height ?? 0
tableView?.contentInset.top = inputAccessoryView?.frame.height ?? 0
}
}

Expand Down Expand Up @@ -341,17 +360,12 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {

if dcChat.canSend {
shouldBecomeFirstResponder = true

if wasInputBarFirstResponder {
messageInputBar.inputTextView.becomeFirstResponder()
} else {
becomeFirstResponder()
}
}

messageInputBar.scrollDownButton.isHidden = true

if isMovingToParent { // being pushed
becomeFirstResponder()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.tableView.contentInset.top = self.inputAccessoryView?.bounds.height ?? 0
if let msgId = self.highlightedMsg, self.messageIds.firstIndex(of: msgId) != nil {
Expand Down Expand Up @@ -384,21 +398,13 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {

// the navigationController will be used when chatDetail is pushed, so we have to remove that gestureRecognizer
navigationController?.navigationBar.removeGestureRecognizer(navBarTap)
wasInputBarFirstResponder = messageInputBar.inputTextView.isFirstResponder
}

override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
AppStateRestorer.shared.resetLastActiveChat()
handleUserVisibility(isVisible: false)
audioController.stopAnyOngoingPlaying()
messageInputBar.inputTextView.resignFirstResponder()
if !wasInputBarFirstResponder {
resignFirstResponder()
}

wasInputBarFirstResponder = false
shouldBecomeFirstResponder = false
}

override func viewSafeAreaInsetsDidChange() {
Expand All @@ -412,7 +418,6 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
if parent == nil {
// going back to previous screen
draft.save(context: dcContext)
keyboardManager = nil
}
}

Expand Down Expand Up @@ -681,7 +686,6 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {

private func configureContactRequestBar() {
messageInputBar.separatorLine.backgroundColor = DcColors.colorDisabled
shouldBecomeFirstResponder = true

let bar: ChatContactRequestBar
if dcChat.isProtectionBroken {
Expand Down Expand Up @@ -745,10 +749,13 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
}

if #available(iOS 13.0, *) {
action.image = UIImage(systemName: "arrowshape.turn.up.left.fill")?.sd_tintedImage(with: DcColors.defaultInverseColor)
action.image = UIImage(systemName: "arrowshape.turn.up.left.fill")?
.sd_tintedImage(with: DcColors.defaultInverseColor)?
.sd_flippedImage(withHorizontal: false, vertical: true)
action.backgroundColor = DcColors.chatBackgroundColor.withAlphaComponent(0.25)
} else {
action.image = UIImage(named: "ic_reply_black")
action.image = UIImage(named: "ic_reply_black")?
.sd_flippedImage(withHorizontal: false, vertical: true)
action.backgroundColor = .systemBlue
}
action.accessibilityElements = nil
Expand Down Expand Up @@ -1242,9 +1249,7 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {

alert.addAction(sendContactAction)

alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: { _ in self.shouldBecomeFirstResponder = true }))

shouldBecomeFirstResponder = false
alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))

self.present(alert, animated: true, completion: {
// unfortunately, voiceMessageAction.accessibilityHint does not work,
Expand Down Expand Up @@ -1481,17 +1486,14 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
}

private func webxdcButtonPressed(_ action: UIAlertAction) {
shouldBecomeFirstResponder = true
showWebxdcSelector()
}

private func documentActionPressed(_ action: UIAlertAction) {
shouldBecomeFirstResponder = true
showDocumentLibrary()
}

private func voiceMessageButtonPressed(_ action: UIAlertAction) {
shouldBecomeFirstResponder = true
showVoiceMessageRecorder()
}

Expand All @@ -1500,13 +1502,10 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
}

private func galleryButtonPressed(_ action: UIAlertAction) {
shouldBecomeFirstResponder = true
showPhotoVideoLibrary(delegate: self)
}

private func showContactList(_ action: UIAlertAction) {
shouldBecomeFirstResponder = true

let contactList = SendContactViewController(dcContext: dcContext)
contactList.delegate = self

Expand All @@ -1522,7 +1521,6 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
}

private func locationStreamingButtonPressed(_ action: UIAlertAction) {
shouldBecomeFirstResponder = true
let isLocationStreaming = dcContext.isSendingLocationsToChat(chatId: chatId)
if isLocationStreaming {
locationStreamingFor(seconds: 0)
Expand Down Expand Up @@ -1775,9 +1773,6 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
}

private func selectMore(at indexPath: IndexPath) {
messageInputBar.inputTextView.resignFirstResponder()
resignFirstResponder()

setEditing(isEditing: true, selectedAtIndexPath: indexPath)
if UIAccessibility.isVoiceOverRunning {
forceVoiceOverFocussingCell(at: indexPath, postingFinished: nil)
Expand Down Expand Up @@ -1901,6 +1896,9 @@ extension ChatViewController {
cell.reactionsView.isHidden = true
contextMenuVisible = true

shouldBecomeFirstResponder = false
messageInputBar.inputTextView.resignFirstResponder()

updateScrollDownButtonVisibility()
}

Expand All @@ -1920,6 +1918,9 @@ extension ChatViewController {
cell.reactionsView.isHidden = false
contextMenuVisible = false

shouldBecomeFirstResponder = true
becomeFirstResponder()

updateScrollDownButtonVisibility()
}

Expand Down Expand Up @@ -2235,8 +2236,6 @@ extension ChatViewController {
_ = handleSelection(indexPath: indexPath)
}
self.updateTitle()
shouldBecomeFirstResponder = isEditing
becomeFirstResponder()
if refreshMessagesAfterEditing && isEditing == false {
refreshMessages()
}
Expand Down
2 changes: 1 addition & 1 deletion deltachat-ios/Chat/Views/DraftArea.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import UIKit
import DcCore

public class DraftArea: UIView, InputItem {
public var inputBarAccessoryView: InputBarAccessoryView?
public weak var inputBarAccessoryView: InputBarAccessoryView?
public var parentStackViewPosition: InputStackView.Position?
public func textViewDidChangeAction(with textView: InputTextView) {}
public func keyboardSwipeGestureAction(with gesture: UISwipeGestureRecognizer) {}
Expand Down
Loading

0 comments on commit 94c35df

Please sign in to comment.