Skip to content

Commit

Permalink
Fix issue (#122) withscrollToRevealFirstResponder (#136)
Browse files Browse the repository at this point in the history
  • Loading branch information
nataliq-pp authored Sep 27, 2019
1 parent e91518b commit a16876e
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 22 deletions.
4 changes: 4 additions & 0 deletions Form.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
5B4ABD3A2257365C0073FACE /* TableKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4ABD392257365C0073FACE /* TableKitTests.swift */; };
721954D821A44E450090F9E3 /* MinimumSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 721954D721A44E450090F9E3 /* MinimumSize.swift */; };
722EB65023266A99003AA360 /* CollectionKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 722EB64F23266A99003AA360 /* CollectionKitTests.swift */; };
722EB63723242D37003AA360 /* ScrollViewVerticalContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 722EB63623242D37003AA360 /* ScrollViewVerticalContextTests.swift */; };
724EC30D2241513D001F3E11 /* UILabel+StylingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 724EC30C2241513D001F3E11 /* UILabel+StylingTests.swift */; };
7270AFB0201FAB7C004DAAA3 /* ViewLayoutArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7270AFAF201FAB7C004DAAA3 /* ViewLayoutArea.swift */; };
72C5ED6F226F432600E32125 /* UIScrollView+PinningTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72C5ED6E226F432600E32125 /* UIScrollView+PinningTests.swift */; };
Expand Down Expand Up @@ -136,6 +137,7 @@
5B4ABD392257365C0073FACE /* TableKitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableKitTests.swift; sourceTree = "<group>"; };
721954D721A44E450090F9E3 /* MinimumSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MinimumSize.swift; path = Form/MinimumSize.swift; sourceTree = "<group>"; };
722EB64F23266A99003AA360 /* CollectionKitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionKitTests.swift; sourceTree = "<group>"; };
722EB63623242D37003AA360 /* ScrollViewVerticalContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewVerticalContextTests.swift; sourceTree = "<group>"; };
724EC30C2241513D001F3E11 /* UILabel+StylingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+StylingTests.swift"; sourceTree = "<group>"; };
7270AFAF201FAB7C004DAAA3 /* ViewLayoutArea.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ViewLayoutArea.swift; path = Form/ViewLayoutArea.swift; sourceTree = "<group>"; };
72C5ED6E226F432600E32125 /* UIScrollView+PinningTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+PinningTests.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -277,6 +279,7 @@
F6B81B9320CA906000B6AC39 /* NumberEditorTests.swift */,
724EC30C2241513D001F3E11 /* UILabel+StylingTests.swift */,
72C5ED6E226F432600E32125 /* UIScrollView+PinningTests.swift */,
722EB63623242D37003AA360 /* ScrollViewVerticalContextTests.swift */,
F62E45B81CABFB5300C6867E /* Info.plist */,
);
path = FormTests;
Expand Down Expand Up @@ -578,6 +581,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
722EB63723242D37003AA360 /* ScrollViewVerticalContextTests.swift in Sources */,
CFD2FFE3221323BF002D4D36 /* TextStyleTests.swift in Sources */,
F604260A20B6A47E00BC4CAB /* ParentChildRelationalTests.swift in Sources */,
F6B81B9420CA906000B6AC39 /* NumberEditorTests.swift in Sources */,
Expand Down
69 changes: 47 additions & 22 deletions Form/UIScrollView+Keyboard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,19 @@ public extension UIScrollView {
let bag = DisposeBag()
var lastResponder = firstResponder

adjustContentOffset(adjustInsets)
self.adjustContentOffsetToRevealFirstResponder(adjustInsets)

bag += keyboardSignal(priority: .contentOffset).onValue { event -> Void in
lastResponder = self.firstResponder
guard case let .willShow(_, animation) = event else { return }
animation.animate { self.adjustContentOffset(adjustInsets) }
animation.animate { self.adjustContentOffsetToRevealFirstResponder(adjustInsets) }
}

bag += NotificationCenter.default.signal(forName: UITextField.textDidBeginEditingNotification).onValue { _ in
DispatchQueue.main.async { // Make sure to run after onKeyboardEvent above.
defer { lastResponder = self.firstResponder }
guard self.firstResponder != lastResponder else { return }
UIView.animate(withDuration: 0.3) { self.adjustContentOffset(adjustInsets) }
UIView.animate(withDuration: 0.3) { self.adjustContentOffsetToRevealFirstResponder(adjustInsets) }
}
}

Expand Down Expand Up @@ -150,33 +150,58 @@ private extension UIScrollView {
}
}

func adjustContentOffset(_ adjustInsets: (UIView) -> UIEdgeInsets) {
guard let firstResponder = firstResponder as? UIView else { return }
func adjustContentOffsetToRevealFirstResponder(_ adjustInsets: (_ firstResponder: UIView) -> UIEdgeInsets) {
if let targetVisibleRect = self.targetVisibleRectToRevealFirstResponder(adjustInsets) {
self.scrollRectToVisible(targetVisibleRect, animated: false)
}
}

func targetVisibleRectToRevealFirstResponder(_ adjustInsets: (_ firstResponder: UIView) -> UIEdgeInsets) -> CGRect? {
guard let frameToFocus = self.firstResponderAdjustedFrame(adjustInsets: adjustInsets) else { return nil }

let verticalFocusPosition = ScrollViewVerticalContext(
visibleRectHeight: self.bounds.height,
visibleRectOffsetY: self.contentOffset.y,
visibleRectInsetTop: self.contentInset.top,
visibleRectInsetBottom: self.contentInset.bottom
).targetFocusPosition(for: frameToFocus)

return verticalFocusPosition.flatMap { CGRect(x: 0, y: $0, width: 1, height: 1) }
}

let viewRect = frame.inset(by: contentInset)
let firstBounds = firstResponder.bounds.inset(by: adjustInsets(firstResponder))
let firstFrame = convert(firstBounds, from: firstResponder)
func firstResponderAdjustedFrame(adjustInsets: (_ firstResponder: UIView) -> UIEdgeInsets) -> CGRect? {
guard let firstResponder = firstResponder as? UIView else { return nil }
let insetAdjustment = adjustInsets(firstResponder)
let frameToFocus = firstResponder.frame.inset(by: insetAdjustment)
return self.convert(frameToFocus, from: firstResponder)
}
}

var portRect = viewRect
portRect.origin.y += contentOffset.y
// Internal helper to move scroll view vertical position calculations outside of the view so that we can test them
struct ScrollViewVerticalContext {
let visibleRectHeight: CGFloat
let visibleRectOffsetY: CGFloat
let visibleRectInsetTop: CGFloat
let visibleRectInsetBottom: CGFloat

// Don't let horizontal values affect contains below.
portRect.origin.x -= 1000
portRect.size.width += 2000
// Calculates the vertical position of the area that needs to become visible for the given frame to be focused.
// It calculates the smallest movement needed based on the relative position on the `frameToFocus`.
// Nil if the frame is already focused.
func targetFocusPosition(for frameToFocus: CGRect) -> CGFloat? {
let visibleRectMinY = visibleRectOffsetY - visibleRectInsetTop
let visibleRectMaxY = visibleRectMinY + visibleRectHeight - visibleRectInsetBottom

guard !portRect.contains(firstFrame) else { return }
let isBelowTop = frameToFocus.minY >= visibleRectMinY
let isAboveBottom = frameToFocus.maxY <= visibleRectMaxY
let isFirstResponderVisible = isBelowTop && isAboveBottom

var offset = contentOffset
let bottom = firstFrame.maxY - viewRect.size.height - contentInset.top
let marginY = layoutMargins.top + contentInset.top
guard !isFirstResponderVisible else { return nil }

if bottom > -max(marginY, firstFrame.height) {
offset.y = bottom
if !isBelowTop {
return max(0, frameToFocus.minY)
} else {
offset.y = -marginY
return max(0, frameToFocus.maxY)
}

setContentOffset(offset, animated: false)
}
}

Expand Down
67 changes: 67 additions & 0 deletions FormTests/ScrollViewVerticalContextTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// UIScrollView+KeyboardTests.swift
// FormTests
//
// Created by Nataliya Patsovska on 2019-09-07.
// Copyright © 2019 iZettle. All rights reserved.
//

import XCTest
@testable import Form
import Flow

class ScrollViewVerticalContextTests: XCTestCase {
func testFocusPosition_noOffsetContext() {
let verticalContext = ScrollViewVerticalContext(
visibleRectHeight: 100,
visibleRectOffsetY: 0,
visibleRectInsetTop: 0,
visibleRectInsetBottom: 0
)

// whitin the visible area
XCTAssertNil(verticalContext.targetFocusPosition(for: CGRect(x: 0, y: 0, width: 100, height: 50)))
XCTAssertNil(verticalContext.targetFocusPosition(for: CGRect(x: 0, y: 50, width: 100, height: 50)))

// below the visible area
XCTAssertEqual(verticalContext.targetFocusPosition(for: CGRect(x: 0, y: 51, width: 100, height: 50)), 101)
}

func testFocusPosition_yOffsetContext() {
let verticalContext = ScrollViewVerticalContext(
visibleRectHeight: 100,
visibleRectOffsetY: 50,
visibleRectInsetTop: 0,
visibleRectInsetBottom: 0
)

// whitin the visible area
XCTAssertNil(verticalContext.targetFocusPosition(for: CGRect(x: 0, y: 50, width: 100, height: 50)))
XCTAssertNil(verticalContext.targetFocusPosition(for: CGRect(x: 0, y: 100, width: 100, height: 50)))

// above the visible area
XCTAssertEqual(verticalContext.targetFocusPosition(for: CGRect(x: 0, y: 49, width: 100, height: 50)), 49)

// below the visible area
XCTAssertEqual(verticalContext.targetFocusPosition(for: CGRect(x: 0, y: 101, width: 100, height: 50)), 151)
}

func testFocusPosition_insettedContext() {
let verticalContext = ScrollViewVerticalContext(
visibleRectHeight: 100,
visibleRectOffsetY: 50,
visibleRectInsetTop: -10,
visibleRectInsetBottom: 20
)

// whitin the visible area
XCTAssertNil(verticalContext.targetFocusPosition(for: CGRect(x: 0, y: 60, width: 100, height: 50)))
XCTAssertNil(verticalContext.targetFocusPosition(for: CGRect(x: 0, y: 90, width: 100, height: 50)))

// above the visible area
XCTAssertEqual(verticalContext.targetFocusPosition(for: CGRect(x: 0, y: 59, width: 100, height: 50)), 59)

// below the visible area
XCTAssertEqual(verticalContext.targetFocusPosition(for: CGRect(x: 0, y: 91, width: 100, height: 50)), 141)
}
}

0 comments on commit a16876e

Please sign in to comment.