From 141eabf4d4c68bb21397fa686d6b7c656dac63a0 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sun, 16 Feb 2020 18:51:12 -0800 Subject: [PATCH 1/4] Add API for keyboard observation on ScrollView --- BlueprintUICommonControls.podspec | 5 + .../Sources/Internal/KeyboardObserver.swift | 178 +++++++++++++++ .../Sources/ScrollView.swift | 206 ++++++++++++++++-- .../Tests/ScrollViewTests.swift | 67 ++++++ .../Internal/KeyboardObserverTests.swift | 182 ++++++++++++++++ SampleApp/Podfile | 4 +- SampleApp/Podfile.lock | 7 +- SampleApp/SampleApp.xcodeproj/project.pbxproj | 136 +++--------- .../xcshareddata/xcschemes/SampleApp.xcscheme | 10 + SampleApp/Sources/AppDelegate.swift | 2 +- .../ScrollViewKeyboardViewController.swift | 49 +++++ 11 files changed, 712 insertions(+), 134 deletions(-) create mode 100644 BlueprintUICommonControls/Sources/Internal/KeyboardObserver.swift create mode 100644 BlueprintUICommonControls/Tests/ScrollViewTests.swift create mode 100644 BlueprintUICommonControls/Tests/Sources/Internal/KeyboardObserverTests.swift create mode 100644 SampleApp/Sources/ScrollViewKeyboardViewController.swift diff --git a/BlueprintUICommonControls.podspec b/BlueprintUICommonControls.podspec index 9c48bbbd0..215c87d44 100644 --- a/BlueprintUICommonControls.podspec +++ b/BlueprintUICommonControls.podspec @@ -14,4 +14,9 @@ Pod::Spec.new do |s| s.source_files = 'BlueprintUICommonControls/Sources/**/*.swift' s.dependency 'BlueprintUI' + + s.test_spec 'Tests' do |test_spec| + test_spec.source_files = 'BlueprintUICommonControls/Tests/**/*.swift' + test_spec.framework = 'XCTest' + end end diff --git a/BlueprintUICommonControls/Sources/Internal/KeyboardObserver.swift b/BlueprintUICommonControls/Sources/Internal/KeyboardObserver.swift new file mode 100644 index 000000000..3f4478344 --- /dev/null +++ b/BlueprintUICommonControls/Sources/Internal/KeyboardObserver.swift @@ -0,0 +1,178 @@ +// +// KeyboardObserver.swift +// BlueprintUICommonControls +// +// Created by Kyle Van Essen on 2/16/20. +// + +import UIKit + + +protocol KeyboardObserverDelegate : AnyObject { + + func keyboardFrameWillChange( + for observer : KeyboardObserver, + animationDuration : Double, + options : UIView.AnimationOptions + ) +} + +/** + Implementation borrowed from Listable: + https://github.com/kyleve/Listable/blob/master/Listable/Sources/Internal/KeyboardObserver.swift + + iOS Docs for keyboard management: + https://developer.apple.com/library/archive/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html + */ +final class KeyboardObserver { + + private let center : NotificationCenter + + weak var delegate : KeyboardObserverDelegate? + + // + // MARK: Initialization + // + + init(center : NotificationCenter = .default) { + + self.center = center + + /// We need to listen to both `will` and `keyboardDidChangeFrame` notifications. Why? + /// When dealing with an undocked or floating keyboard, moving the keyboard + /// around the screen does NOT call `willChangeFrame`; only `didChangeFrame` is called. + /// Before calling the delegate, we compare `old.endingFrame != new.endingFrame`, + /// which ensures that the delegate is notified if the frame really changes, and + /// prevents duplicate calls. + + self.center.addObserver(self, selector: #selector(keyboardFrameChanged(_:)), name: UIWindow.keyboardWillChangeFrameNotification, object: nil) + self.center.addObserver(self, selector: #selector(keyboardFrameChanged(_:)), name: UIWindow.keyboardDidChangeFrameNotification, object: nil) + } + + private var latestNotification : NotificationInfo? + + // + // MARK: Handling Changes + // + + enum KeyboardFrame : Equatable { + + /// The current frame does not overlap the current view at all. + case nonOverlapping + + /// The current frame does overlap the view, by the provided rect, in the view's coordinate space. + case visible(frame: CGRect) + } + + /// How the keyboard overlaps the view provided. If the view is not on screen (eg, no window), + /// or the observer has not yet learned about the keyboard's position, this method returns nil. + func currentFrame(in view : UIView) -> KeyboardFrame? { + + guard view.window != nil else { + return nil + } + + guard let notification = self.latestNotification else { + return nil + } + + let frame = view.convert(notification.endingFrame, from: nil) + + if frame.intersects(view.bounds) { + return .visible(frame: frame) + } else { + return .nonOverlapping + } + } + + // + // MARK: Receiving Updates + // + + private func receivedUpdatedKeyboardInfo(_ new : NotificationInfo) { + + let old = self.latestNotification + + self.latestNotification = new + + /// Only communicate a frame change to the delegate if the frame actually changed. + + if let old = old { + guard old.endingFrame != new.endingFrame else { + return + } + } + + /** + Create an animation curve with the correct curve for showing or hiding the keyboard. + + This is unfortunately a private UIView curve. However, we can map it to the animation options' curve + like so: https://stackoverflow.com/questions/26939105/keyboard-animation-curve-as-int + */ + let animationOptions = UIView.AnimationOptions(rawValue: new.animationCurve << 16) + + self.delegate?.keyboardFrameWillChange( + for: self, + animationDuration: new.animationDuration, + options: animationOptions + ) + } + + // + // MARK: Notification Listeners + // + + @objc private func keyboardFrameChanged(_ notification : Notification) { + + do { + let info = try NotificationInfo(with: notification) + self.receivedUpdatedKeyboardInfo(info) + } catch { + assertionFailure("Blueprint could not read system keyboard notification. This error needs to be fixed in Blueprint. Error: \(error)") + } + } +} + +extension KeyboardObserver +{ + struct NotificationInfo : Equatable { + + var endingFrame : CGRect = .zero + + var animationDuration : Double = 0.0 + var animationCurve : UInt = 0 + + init(with notification : Notification) throws { + + guard let userInfo = notification.userInfo else { + throw ParseError.missingUserInfo + } + + guard let endingFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { + throw ParseError.missingEndingFrame + } + + self.endingFrame = endingFrame + + guard let animationDuration = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue else { + throw ParseError.missingAnimationDuration + } + + self.animationDuration = animationDuration + + guard let animationCurve = (userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue else { + throw ParseError.missingAnimationCurve + } + + self.animationCurve = animationCurve + } + + enum ParseError : Error, Equatable { + + case missingUserInfo + case missingEndingFrame + case missingAnimationDuration + case missingAnimationCurve + } + } +} diff --git a/BlueprintUICommonControls/Sources/ScrollView.swift b/BlueprintUICommonControls/Sources/ScrollView.swift index 3f7d5c1cd..6a236626a 100644 --- a/BlueprintUICommonControls/Sources/ScrollView.swift +++ b/BlueprintUICommonControls/Sources/ScrollView.swift @@ -12,11 +12,21 @@ public struct ScrollView: Element { public var contentSize: ContentSize = .fittingHeight public var alwaysBounceVertical = false public var alwaysBounceHorizontal = false + + /** + How much the content of the `ScrollView` should be inset. + + Note: When `keyboardAdjustmentMode` is used, it will also adjust + the on-screen `UIScrollView`s `contentInset.bottom` to make space for the keyboard. + */ public var contentInset: UIEdgeInsets = .zero + public var centersUnderflow: Bool = false public var showsHorizontalScrollIndicator: Bool = true public var showsVerticalScrollIndicator: Bool = true public var pullToRefreshBehavior: PullToRefreshBehavior = .disabled + public var keyboardDismissMode: UIScrollView.KeyboardDismissMode = .none + public var keyboardAdjustmentMode : KeyboardAdjustmentMode = .adjustsWhenVisible public init(wrapping element: Element) { self.wrappedElement = element @@ -28,10 +38,15 @@ public struct ScrollView: Element { public func backingViewDescription(bounds: CGRect, subtreeExtent: CGRect?) -> ViewDescription? { return ScrollerWrapperView.describe { config in + config.builder = { + ScrollerWrapperView(frame: bounds, representedElement: self) + } + config.contentView = { $0.scrollView } - config.apply({ (view) in - view.apply(scrollView: self, contentFrame: subtreeExtent ?? .zero) - }) + + config.apply { + $0.apply(scrollView: self, contentFrame: subtreeExtent ?? .zero) + } } } @@ -39,11 +54,9 @@ public struct ScrollView: Element { return Layout( contentInset: contentInset, contentSize: contentSize, - centersUnderflow: centersUnderflow) + centersUnderflow: centersUnderflow + ) } - - - } extension ScrollView { @@ -96,6 +109,11 @@ extension ScrollView { } extension ScrollView { + + public enum KeyboardAdjustmentMode : Equatable { + case none + case adjustsWhenVisible + } public enum ContentSize : Equatable { @@ -144,6 +162,10 @@ extension ScrollView { fileprivate final class ScrollerWrapperView: UIView { let scrollView = UIScrollView() + let keyboardObserver = KeyboardObserver() + + /// The current `ScrollView` state we represent. + private var representedElement : ScrollView private var refreshControl: UIRefreshControl? = nil { @@ -162,8 +184,14 @@ fileprivate final class ScrollerWrapperView: UIView { private var refreshAction: () -> Void = { } - override init(frame: CGRect) { + init(frame: CGRect, representedElement : ScrollView) { + + self.representedElement = representedElement + super.init(frame: frame) + + self.keyboardObserver.delegate = self + addSubview(scrollView) } @@ -183,6 +211,8 @@ fileprivate final class ScrollerWrapperView: UIView { func apply(scrollView: ScrollView, contentFrame: CGRect) { + self.representedElement = scrollView + switch scrollView.pullToRefreshBehavior { case .disabled, .refreshing: refreshAction = { } @@ -237,16 +267,29 @@ fileprivate final class ScrollerWrapperView: UIView { if self.scrollView.showsHorizontalScrollIndicator != scrollView.showsHorizontalScrollIndicator { self.scrollView.showsHorizontalScrollIndicator = scrollView.showsHorizontalScrollIndicator } - - var contentInset = scrollView.contentInset - - if case .refreshing = scrollView.pullToRefreshBehavior, let refreshControl = refreshControl { - // The refresh control lives above the content and adjusts the - // content inset for itself when visible. Do the same adjustment to - // our expected content inset. - contentInset.top += refreshControl.bounds.height + + if self.scrollView.keyboardDismissMode != scrollView.keyboardDismissMode { + self.scrollView.keyboardDismissMode = scrollView.keyboardDismissMode } - + + self.applyContentInset(with: scrollView) + } + + private func applyContentInset(with scrollView : ScrollView) + { + // Keep a copy of the content inset as provided by the developer. + // We will use this when adjusting the `contentInset.bottom` for the keyboard. + + let contentInset = ScrollView.finalContentInset( + scrollViewInsets: scrollView.contentInset, + safeAreaInsets: self.bp_safeAreaInsets, + keyboardBottomInset: self.bottomContentInsetAdjustmentForKeyboard, + refreshControlState: scrollView.pullToRefreshBehavior, + refreshControlBounds: refreshControl?.bounds + ) + + // Apply the updated contentInset if it changed. + if self.scrollView.contentInset != contentInset { let wasScrolledToTop = self.scrollView.contentOffset.y == -self.scrollView.contentInset.top @@ -262,8 +305,137 @@ fileprivate final class ScrollerWrapperView: UIView { self.scrollView.contentOffset.x = -contentInset.left } } + } + + // + // MARK: UIView + // + + public override func didMoveToWindow() { + super.didMoveToWindow() + + if self.window != nil { + self.updateBottomContentInsetWithKeyboardFrame() + } + } + + public override func didMoveToSuperview() { + super.didMoveToSuperview() + + if self.superview != nil { + self.updateBottomContentInsetWithKeyboardFrame() + } + } +} +extension ScrollView +{ + // Calculates the correct content inset to apply for the given inputs. + + static func finalContentInset( + scrollViewInsets : UIEdgeInsets, + safeAreaInsets : UIEdgeInsets, + keyboardBottomInset : CGFloat, + refreshControlState : PullToRefreshBehavior, + refreshControlBounds : CGRect? + ) -> UIEdgeInsets + { + var finalContentInset = scrollViewInsets + + // Include the keyboard's adjustment at the bottom of the scroll view. + + if keyboardBottomInset > 0.0 { + finalContentInset.bottom += keyboardBottomInset + + // Exclude the safe area insets, so the content hugs the top of the keyboard. + + finalContentInset.bottom -= safeAreaInsets.bottom + } + + // The refresh control lives above the content and adjusts the + // content inset for itself when visible and refreshing. + // Do the same adjustment to our expected content inset. + + if case .refreshing = refreshControlState { + finalContentInset.top += refreshControlBounds?.size.height ?? 0.0 + } + + return finalContentInset } +} + +extension ScrollerWrapperView : KeyboardObserverDelegate { + + // + // MARK: Keyboard + // + + private func updateBottomContentInsetWithKeyboardFrame() { + + let contentInset = ScrollView.finalContentInset( + scrollViewInsets: self.representedElement.contentInset, + safeAreaInsets: self.bp_safeAreaInsets, + keyboardBottomInset: self.bottomContentInsetAdjustmentForKeyboard, + refreshControlState: self.representedElement.pullToRefreshBehavior, + refreshControlBounds: self.refreshControl?.bounds + ) + + /// Setting contentInset, even to the same value, can cause issues during scrolling (such as stopping scrolling). + /// Make sure we're only assigning the value if it changed. + + if self.scrollView.contentInset.bottom != contentInset.bottom { + self.scrollView.contentInset.bottom = contentInset.bottom + } + } + + fileprivate var bottomContentInsetAdjustmentForKeyboard : CGFloat { + + switch self.representedElement.keyboardAdjustmentMode { + case .none: + return 0.0 + + case .adjustsWhenVisible: + guard let keyboardFrame = self.keyboardObserver.currentFrame(in: self) else { + return 0.0 + } + + return { + switch keyboardFrame { + case .nonOverlapping: + return 0.0 + + case .visible(let frame): + return self.bounds.size.height - frame.origin.y + } + }() + } + } + + // + // MARK: KeyboardObserverDelegate + // + + func keyboardFrameWillChange( + for observer : KeyboardObserver, + animationDuration : Double, + options : UIView.AnimationOptions + ) { + UIView.animate(withDuration: animationDuration, delay: 0.0, options: options, animations: { + self.updateBottomContentInsetWithKeyboardFrame() + }) + } +} + + +private extension UIView { + + var bp_safeAreaInsets : UIEdgeInsets { + if #available(iOS 11.0, *) { + return self.safeAreaInsets + } else { + return .zero + } + } } diff --git a/BlueprintUICommonControls/Tests/ScrollViewTests.swift b/BlueprintUICommonControls/Tests/ScrollViewTests.swift new file mode 100644 index 000000000..fcd7be016 --- /dev/null +++ b/BlueprintUICommonControls/Tests/ScrollViewTests.swift @@ -0,0 +1,67 @@ +// +// ScrollViewTests.swift +// BlueprintUICommonControls-Unit-Tests +// +// Created by Kyle Van Essen on 2/26/20. +// + +import Foundation +import XCTest + +import BlueprintUI + +@testable import BlueprintUICommonControls + + +class ScrollViewTests : XCTestCase { + + func test_contentInset() { + + let view = BlueprintView() + } + + func test_finalContentInset() + { + // No inset + + XCTAssertEqual( + UIEdgeInsets.zero, + + ScrollView.finalContentInset( + scrollViewInsets: .zero, + safeAreaInsets: UIEdgeInsets(top: 10.0, left: 11.0, bottom: 12.0, right: 13.0), + keyboardBottomInset: .zero, + refreshControlState: .disabled, + refreshControlBounds: CGRect(origin: .zero, size: CGSize(width: 25.0, height: 25.0)) + ) + ) + + // Keyboard Inset + + XCTAssertEqual( + UIEdgeInsets(top: 10.0, left: 11.0, bottom: 50.0, right: 13.0), + + ScrollView.finalContentInset( + scrollViewInsets: UIEdgeInsets(top: 10.0, left: 11.0, bottom: 12.0, right: 13.0), + safeAreaInsets: UIEdgeInsets(top: 10.0, left: 11.0, bottom: 12.0, right: 13.0), + keyboardBottomInset: 50.0, + refreshControlState: .disabled, + refreshControlBounds: CGRect(origin: .zero, size: CGSize(width: 25.0, height: 25.0)) + ) + ) + + // Keyboard Inset and refreshing state + + XCTAssertEqual( + UIEdgeInsets(top: 35.0, left: 11.0, bottom:50.0, right: 13.0), + + ScrollView.finalContentInset( + scrollViewInsets: UIEdgeInsets(top: 10.0, left: 11.0, bottom: 12.0, right: 13.0), + safeAreaInsets: UIEdgeInsets(top: 10.0, left: 11.0, bottom: 12.0, right: 13.0), + keyboardBottomInset: 50.0, + refreshControlState: .refreshing, + refreshControlBounds: CGRect(origin: .zero, size: CGSize(width: 25.0, height: 25.0)) + ) + ) + } +} diff --git a/BlueprintUICommonControls/Tests/Sources/Internal/KeyboardObserverTests.swift b/BlueprintUICommonControls/Tests/Sources/Internal/KeyboardObserverTests.swift new file mode 100644 index 000000000..a746a091d --- /dev/null +++ b/BlueprintUICommonControls/Tests/Sources/Internal/KeyboardObserverTests.swift @@ -0,0 +1,182 @@ +import XCTest +import UIKit + +@testable import BlueprintUICommonControls + + +class KeyboardObserverTests: XCTestCase { + + func test_notifications() { + let center = NotificationCenter() + + self.testcase("Will Change Frame") { + let observer = KeyboardObserver(center: center) + + let delegate = Delegate() + observer.delegate = delegate + + let userInfo : [AnyHashable:Any] = [ + UIResponder.keyboardFrameEndUserInfoKey : NSValue(cgRect: CGRect(x: 10.0, y: 10.0, width: 100.0, height: 200.0)), + UIResponder.keyboardAnimationDurationUserInfoKey : NSNumber(value: 2.5), + UIResponder.keyboardAnimationCurveUserInfoKey : NSNumber(value: 123) + ] + + XCTAssertEqual(delegate.keyboardFrameWillChange_callCount, 0) + center.post(Notification(name: UIWindow.keyboardWillChangeFrameNotification, object: nil, userInfo: userInfo)) + XCTAssertEqual(delegate.keyboardFrameWillChange_callCount, 1) + } + + self.testcase("Did Change Frame") { + let observer = KeyboardObserver(center: center) + + let delegate = Delegate() + observer.delegate = delegate + + let userInfo : [AnyHashable:Any] = [ + UIResponder.keyboardFrameEndUserInfoKey : NSValue(cgRect: CGRect(x: 10.0, y: 10.0, width: 100.0, height: 200.0)), + UIResponder.keyboardAnimationDurationUserInfoKey : NSNumber(value: 2.5), + UIResponder.keyboardAnimationCurveUserInfoKey : NSNumber(value: 123) + ] + + XCTAssertEqual(delegate.keyboardFrameWillChange_callCount, 0) + center.post(Notification(name: UIWindow.keyboardDidChangeFrameNotification, object: nil, userInfo: userInfo)) + XCTAssertEqual(delegate.keyboardFrameWillChange_callCount, 1) + } + + self.testcase("Only calls delegate for changed frame") { + let observer = KeyboardObserver(center: center) + + let delegate = Delegate() + observer.delegate = delegate + + let userInfo : [AnyHashable:Any] = [ + UIResponder.keyboardFrameEndUserInfoKey : NSValue(cgRect: CGRect(x: 10.0, y: 10.0, width: 100.0, height: 200.0)), + UIResponder.keyboardAnimationDurationUserInfoKey : NSNumber(value: 2.5), + UIResponder.keyboardAnimationCurveUserInfoKey : NSNumber(value: 123) + ] + + XCTAssertEqual(delegate.keyboardFrameWillChange_callCount, 0) + center.post(Notification(name: UIWindow.keyboardDidChangeFrameNotification, object: nil, userInfo: userInfo)) + XCTAssertEqual(delegate.keyboardFrameWillChange_callCount, 1) + + XCTAssertEqual(delegate.keyboardFrameWillChange_callCount, 1) + center.post(Notification(name: UIWindow.keyboardDidChangeFrameNotification, object: nil, userInfo: userInfo)) + XCTAssertEqual(delegate.keyboardFrameWillChange_callCount, 1) + } + } + + final class Delegate : KeyboardObserverDelegate { + + var keyboardFrameWillChange_callCount : Int = 0 + + func keyboardFrameWillChange(for observer: KeyboardObserver, animationDuration: Double, options: UIView.AnimationOptions) { + + self.keyboardFrameWillChange_callCount += 1 + } + } +} + + +class KeyboardObserver_NotificationInfo_Tests : XCTestCase { + + func test_init() { + + let defaultUserInfo : [AnyHashable:Any] = [ + UIResponder.keyboardFrameEndUserInfoKey : NSValue(cgRect: CGRect(x: 10.0, y: 10.0, width: 100.0, height: 200.0)), + UIResponder.keyboardAnimationDurationUserInfoKey : NSNumber(value: 2.5), + UIResponder.keyboardAnimationCurveUserInfoKey : NSNumber(value: 123) + ] + + self.testcase("Successful Init") { + let info = try! KeyboardObserver.NotificationInfo( + with: Notification(name: UIResponder.keyboardDidShowNotification, object: nil, userInfo: defaultUserInfo) + ) + + XCTAssertEqual(info.endingFrame, CGRect(x: 10.0, y: 10.0, width: 100.0, height: 200.0)) + XCTAssertEqual(info.animationDuration, 2.5) + XCTAssertEqual(info.animationCurve, 123) + } + + self.testcase("Failed Inits") { + + self.testcase("No userInfo") { + self.assertThrowsError(test: { + try _ = KeyboardObserver.NotificationInfo( + with: Notification(name: UIResponder.keyboardDidShowNotification, object: nil, userInfo: nil) + ) + }, verify: { error in + XCTAssertEqual(error as? KeyboardObserver.NotificationInfo.ParseError, .missingUserInfo) + }) + } + + self.testcase("No end frame") { + + var userInfo = defaultUserInfo + userInfo.removeValue(forKey: UIResponder.keyboardFrameEndUserInfoKey) + + self.assertThrowsError(test: { + try _ = KeyboardObserver.NotificationInfo( + with: Notification(name: UIResponder.keyboardDidShowNotification, object: nil, userInfo: userInfo) + ) + }, verify: { error in + XCTAssertEqual(error as? KeyboardObserver.NotificationInfo.ParseError, .missingEndingFrame) + }) + } + + self.testcase("No animation duration") { + + var userInfo = defaultUserInfo + userInfo.removeValue(forKey: UIResponder.keyboardAnimationDurationUserInfoKey) + + self.assertThrowsError(test: { + try _ = KeyboardObserver.NotificationInfo( + with: Notification(name: UIResponder.keyboardDidShowNotification, object: nil, userInfo: userInfo) + ) + }, verify: { error in + XCTAssertEqual(error as? KeyboardObserver.NotificationInfo.ParseError, .missingAnimationDuration) + }) + } + + self.testcase("No animation curve") { + + var userInfo = defaultUserInfo + userInfo.removeValue(forKey: UIResponder.keyboardAnimationCurveUserInfoKey) + + XCTAssertThrowsError( + try KeyboardObserver.NotificationInfo( + with: Notification(name: UIResponder.keyboardDidShowNotification, object: nil, userInfo: userInfo) + ) + ) { error in + XCTAssertEqual(error as? KeyboardObserver.NotificationInfo.ParseError, .missingAnimationCurve) + } + } + + } + } +} + + +extension XCTestCase { + + func testcase(_ name : String = "", _ block : () throws -> ()) { + do { + try block() + } catch { + XCTFail("Unexpected error thrown: \(error)") + } + } + + func assertThrowsError(test : () throws -> (), verify : (Error) -> ()) { + + var thrown = false + + do { + try test() + } catch { + thrown = true + verify(error) + } + + XCTAssertTrue(thrown, "Expected an error to be thrown but one was not.") + } +} diff --git a/SampleApp/Podfile b/SampleApp/Podfile index b6a6963c0..175fd8cb4 100644 --- a/SampleApp/Podfile +++ b/SampleApp/Podfile @@ -1,12 +1,10 @@ platform :ios, '12.0' -inhibit_all_warnings! -use_frameworks! project 'SampleApp.xcodeproj' def blueprint_pods pod 'BlueprintUI', :path => '../BlueprintUI.podspec', :testspecs => ['Tests'] - pod 'BlueprintUICommonControls', :path => '../BlueprintUICommonControls.podspec', :testspecs => [] + pod 'BlueprintUICommonControls', :path => '../BlueprintUICommonControls.podspec', :testspecs => ['Tests'] end target 'SampleApp' do diff --git a/SampleApp/Podfile.lock b/SampleApp/Podfile.lock index 23eb141da..93607990d 100644 --- a/SampleApp/Podfile.lock +++ b/SampleApp/Podfile.lock @@ -3,11 +3,14 @@ PODS: - BlueprintUI/Tests (0.5.1) - BlueprintUICommonControls (0.5.1): - BlueprintUI + - BlueprintUICommonControls/Tests (0.5.1): + - BlueprintUI DEPENDENCIES: - BlueprintUI (from `../BlueprintUI.podspec`) - BlueprintUI/Tests (from `../BlueprintUI.podspec`) - BlueprintUICommonControls (from `../BlueprintUICommonControls.podspec`) + - BlueprintUICommonControls/Tests (from `../BlueprintUICommonControls.podspec`) EXTERNAL SOURCES: BlueprintUI: @@ -17,8 +20,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: BlueprintUI: e5f89d442f225e1865aa8f153dc9da3f1adcbfc4 - BlueprintUICommonControls: d70a7f34ee88242d3b5e13e81de92ca326c95bec + BlueprintUICommonControls: 081862011936eaad26f8d606803fa9539ce188ab -PODFILE CHECKSUM: e9562cee84054ca8676daa6bfce31541238548f0 +PODFILE CHECKSUM: 1e8513f2ddc9f8511335f2c2c2378c4da8250058 COCOAPODS: 1.8.4 diff --git a/SampleApp/SampleApp.xcodeproj/project.pbxproj b/SampleApp/SampleApp.xcodeproj/project.pbxproj index 47581c004..ba9245523 100644 --- a/SampleApp/SampleApp.xcodeproj/project.pbxproj +++ b/SampleApp/SampleApp.xcodeproj/project.pbxproj @@ -3,12 +3,15 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ - 5C40B326942B3D4913788CB6 /* Pods_Tutorial_1__Completed_.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 12F17E90CFF5B03DC755C199 /* Pods_Tutorial_1__Completed_.framework */; }; - 6D764555401AA55AC666B8E9 /* Pods_Tutorial_2__Completed_.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 999BE36CC630B1FB1221C6A4 /* Pods_Tutorial_2__Completed_.framework */; }; + 0AEA09B32428360500F9ED0C /* ScrollViewKeyboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AEA09B22428360500F9ED0C /* ScrollViewKeyboardViewController.swift */; }; + 4BA6A23CEA6E020DE2B93BAC /* libPods-SampleApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EBB02D3CED9FE1C62118A89 /* libPods-SampleApp.a */; }; + 6CD727ED2241C847FE76071E /* libPods-Tutorial 2 (Completed).a in Frameworks */ = {isa = PBXBuildFile; fileRef = 38F296775FF2518345AB3362 /* libPods-Tutorial 2 (Completed).a */; }; + 7BFF1CD68EA598E1D1C72310 /* libPods-Tutorial 1 (Completed).a in Frameworks */ = {isa = PBXBuildFile; fileRef = BCB39D955649A4F41257087E /* libPods-Tutorial 1 (Completed).a */; }; + 827C9B72171DA5EAC8DDF0A6 /* libPods-Tutorial 1.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BDF1B9D873CF002855E84943 /* libPods-Tutorial 1.a */; }; 97545519223C12E9003E353F /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97545510223C12E9003E353F /* ViewController.swift */; }; 9754551A223C12E9003E353F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97545511223C12E9003E353F /* AppDelegate.swift */; }; 9754551B223C12E9003E353F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97545513223C12E9003E353F /* Assets.xcassets */; }; @@ -32,20 +35,18 @@ 979F49ED224D1BD300A3C5D4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97545513223C12E9003E353F /* Assets.xcassets */; }; 979F49EE224D1BD300A3C5D4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97545514223C12E9003E353F /* LaunchScreen.storyboard */; }; 979F49F4224D1BF200A3C5D4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 979F49E4224D1B6C00A3C5D4 /* AppDelegate.swift */; }; - AAD955648BBE7A5F84E13693 /* Pods_Tutorial_2.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 17497656EE201EC67FE1279C /* Pods_Tutorial_2.framework */; }; - AC5D219CD6E6C652D3F91B9C /* Pods_SampleApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9D2E6D49C3744CB84AA716AB /* Pods_SampleApp.framework */; }; - CECC325B3B40C491BCB6DD06 /* Pods_Tutorial_1.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 291ED687ECA8BACE04E47989 /* Pods_Tutorial_1.framework */; }; + A9B5241771B561530B060284 /* libPods-Tutorial 2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A01464A24580A2CFB10C4C23 /* libPods-Tutorial 2.a */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 0AEA09B22428360500F9ED0C /* ScrollViewKeyboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewKeyboardViewController.swift; sourceTree = ""; }; 0DA29F056002872418F7D2C9 /* Pods-SampleApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleApp.debug.xcconfig"; path = "Target Support Files/Pods-SampleApp/Pods-SampleApp.debug.xcconfig"; sourceTree = ""; }; - 12F17E90CFF5B03DC755C199 /* Pods_Tutorial_1__Completed_.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Tutorial_1__Completed_.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 17497656EE201EC67FE1279C /* Pods_Tutorial_2.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Tutorial_2.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1A147478D522E763BFC19F37 /* Pods-Tutorial 2 (Completed).debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tutorial 2 (Completed).debug.xcconfig"; path = "Target Support Files/Pods-Tutorial 2 (Completed)/Pods-Tutorial 2 (Completed).debug.xcconfig"; sourceTree = ""; }; - 291ED687ECA8BACE04E47989 /* Pods_Tutorial_1.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Tutorial_1.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2C24AF26BD9F959834BD0A25 /* Pods-Tutorial1.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tutorial1.debug.xcconfig"; path = "../../Pods/Target Support Files/Pods-Tutorial1/Pods-Tutorial1.debug.xcconfig"; sourceTree = ""; }; 2F2123B124F06446978E629D /* Pods-SampleApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleApp.release.xcconfig"; path = "Target Support Files/Pods-SampleApp/Pods-SampleApp.release.xcconfig"; sourceTree = ""; }; + 38F296775FF2518345AB3362 /* libPods-Tutorial 2 (Completed).a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Tutorial 2 (Completed).a"; sourceTree = BUILT_PRODUCTS_DIR; }; 6D524EC9EF929FCE2F54EC85 /* Pods-Tutorial1.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tutorial1.release.xcconfig"; path = "../../Pods/Target Support Files/Pods-Tutorial1/Pods-Tutorial1.release.xcconfig"; sourceTree = ""; }; + 6EBB02D3CED9FE1C62118A89 /* libPods-SampleApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SampleApp.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 70E2DBB7A375C527D66B2643 /* Pods-Tutorial 1.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tutorial 1.debug.xcconfig"; path = "Target Support Files/Pods-Tutorial 1/Pods-Tutorial 1.debug.xcconfig"; sourceTree = ""; }; 7A5624391C38E617246C4356 /* Pods-Tutorial 2.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tutorial 2.debug.xcconfig"; path = "Target Support Files/Pods-Tutorial 2/Pods-Tutorial 2.debug.xcconfig"; sourceTree = ""; }; 7EA5EE31421ECA2CA9457450 /* Pods-Tutorial 2.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tutorial 2.release.xcconfig"; path = "Target Support Files/Pods-Tutorial 2/Pods-Tutorial 2.release.xcconfig"; sourceTree = ""; }; @@ -71,9 +72,10 @@ 9796EC7F224DD67900E729F3 /* Purchase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Purchase.swift; sourceTree = ""; }; 979F49E4224D1B6C00A3C5D4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 979F49F2224D1BD300A3C5D4 /* Tutorial 1.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Tutorial 1.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 999BE36CC630B1FB1221C6A4 /* Pods_Tutorial_2__Completed_.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Tutorial_2__Completed_.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 9D2E6D49C3744CB84AA716AB /* Pods_SampleApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SampleApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9D59D92E955A10C9F7EF0730 /* Pods-Tutorial 2 (Completed).release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tutorial 2 (Completed).release.xcconfig"; path = "Target Support Files/Pods-Tutorial 2 (Completed)/Pods-Tutorial 2 (Completed).release.xcconfig"; sourceTree = ""; }; + A01464A24580A2CFB10C4C23 /* libPods-Tutorial 2.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Tutorial 2.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + BCB39D955649A4F41257087E /* libPods-Tutorial 1 (Completed).a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Tutorial 1 (Completed).a"; sourceTree = BUILT_PRODUCTS_DIR; }; + BDF1B9D873CF002855E84943 /* libPods-Tutorial 1.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Tutorial 1.a"; sourceTree = BUILT_PRODUCTS_DIR; }; CC5FC85BE57034BE39916388 /* Pods-Tutorial 1 (Completed).debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tutorial 1 (Completed).debug.xcconfig"; path = "Target Support Files/Pods-Tutorial 1 (Completed)/Pods-Tutorial 1 (Completed).debug.xcconfig"; sourceTree = ""; }; F211AAFE0FF4DC614FC65630 /* Pods-Tutorial 1 (Completed).release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tutorial 1 (Completed).release.xcconfig"; path = "Target Support Files/Pods-Tutorial 1 (Completed)/Pods-Tutorial 1 (Completed).release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -83,7 +85,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - AC5D219CD6E6C652D3F91B9C /* Pods_SampleApp.framework in Frameworks */, + 4BA6A23CEA6E020DE2B93BAC /* libPods-SampleApp.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -91,7 +93,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5C40B326942B3D4913788CB6 /* Pods_Tutorial_1__Completed_.framework in Frameworks */, + 7BFF1CD68EA598E1D1C72310 /* libPods-Tutorial 1 (Completed).a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -99,7 +101,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - AAD955648BBE7A5F84E13693 /* Pods_Tutorial_2.framework in Frameworks */, + A9B5241771B561530B060284 /* libPods-Tutorial 2.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -107,7 +109,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6D764555401AA55AC666B8E9 /* Pods_Tutorial_2__Completed_.framework in Frameworks */, + 6CD727ED2241C847FE76071E /* libPods-Tutorial 2 (Completed).a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -115,7 +117,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - CECC325B3B40C491BCB6DD06 /* Pods_Tutorial_1.framework in Frameworks */, + 827C9B72171DA5EAC8DDF0A6 /* libPods-Tutorial 1.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,11 +127,11 @@ 69943C07C9B6B3DCFC875540 /* Frameworks */ = { isa = PBXGroup; children = ( - 9D2E6D49C3744CB84AA716AB /* Pods_SampleApp.framework */, - 291ED687ECA8BACE04E47989 /* Pods_Tutorial_1.framework */, - 12F17E90CFF5B03DC755C199 /* Pods_Tutorial_1__Completed_.framework */, - 17497656EE201EC67FE1279C /* Pods_Tutorial_2.framework */, - 999BE36CC630B1FB1221C6A4 /* Pods_Tutorial_2__Completed_.framework */, + 6EBB02D3CED9FE1C62118A89 /* libPods-SampleApp.a */, + BDF1B9D873CF002855E84943 /* libPods-Tutorial 1.a */, + BCB39D955649A4F41257087E /* libPods-Tutorial 1 (Completed).a */, + A01464A24580A2CFB10C4C23 /* libPods-Tutorial 2.a */, + 38F296775FF2518345AB3362 /* libPods-Tutorial 2 (Completed).a */, ); name = Frameworks; sourceTree = ""; @@ -162,6 +164,7 @@ isa = PBXGroup; children = ( 97545510223C12E9003E353F /* ViewController.swift */, + 0AEA09B22428360500F9ED0C /* ScrollViewKeyboardViewController.swift */, 97545511223C12E9003E353F /* AppDelegate.swift */, ); path = Sources; @@ -258,7 +261,6 @@ 975454F6223C1289003E353F /* Sources */, 975454F7223C1289003E353F /* Frameworks */, 975454F8223C1289003E353F /* Resources */, - 89E3A01F910F555826A424C0 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -277,7 +279,6 @@ 9796EC3D224D1D2000E729F3 /* Sources */, 9796EC3F224D1D2000E729F3 /* Frameworks */, 9796EC41224D1D2000E729F3 /* Resources */, - 80D652A488CC04A041B8853F /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -296,7 +297,6 @@ 9796EC54224DB3BF00E729F3 /* Sources */, 9796EC57224DB3BF00E729F3 /* Frameworks */, 9796EC59224DB3BF00E729F3 /* Resources */, - 3D033B702F602ECCEEAAE356 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -315,7 +315,6 @@ 9796EC63224DB3C500E729F3 /* Sources */, 9796EC66224DB3C500E729F3 /* Frameworks */, 9796EC68224DB3C500E729F3 /* Resources */, - 5E7D6FD575883BD3D957679B /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -334,7 +333,6 @@ 979F49E7224D1BD300A3C5D4 /* Sources */, 979F49EA224D1BD300A3C5D4 /* Frameworks */, 979F49EC224D1BD300A3C5D4 /* Resources */, - 540EABB49D00E65D64B6FAE7 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -444,40 +442,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3D033B702F602ECCEEAAE356 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Tutorial 2/Pods-Tutorial 2-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Tutorial 2/Pods-Tutorial 2-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Tutorial 2/Pods-Tutorial 2-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 540EABB49D00E65D64B6FAE7 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Tutorial 1/Pods-Tutorial 1-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Tutorial 1/Pods-Tutorial 1-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Tutorial 1/Pods-Tutorial 1-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 5C44B0027B3C66DF83F44D0A /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -500,23 +464,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 5E7D6FD575883BD3D957679B /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Tutorial 2 (Completed)/Pods-Tutorial 2 (Completed)-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Tutorial 2 (Completed)/Pods-Tutorial 2 (Completed)-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Tutorial 2 (Completed)/Pods-Tutorial 2 (Completed)-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 6DD21A891D7570215C38D645 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -539,40 +486,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 80D652A488CC04A041B8853F /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Tutorial 1 (Completed)/Pods-Tutorial 1 (Completed)-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Tutorial 1 (Completed)/Pods-Tutorial 1 (Completed)-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Tutorial 1 (Completed)/Pods-Tutorial 1 (Completed)-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 89E3A01F910F555826A424C0 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 9796EC3C224D1D2000E729F3 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -648,6 +561,7 @@ files = ( 9754551A223C12E9003E353F /* AppDelegate.swift in Sources */, 97545519223C12E9003E353F /* ViewController.swift in Sources */, + 0AEA09B32428360500F9ED0C /* ScrollViewKeyboardViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SampleApp/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleApp.xcscheme b/SampleApp/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleApp.xcscheme index 4c3b622c5..d03cf2574 100644 --- a/SampleApp/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleApp.xcscheme +++ b/SampleApp/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleApp.xcscheme @@ -47,6 +47,16 @@ ReferencedContainer = "container:Pods/Pods.xcodeproj"> + + + + Bool { window = UIWindow(frame: UIScreen.main.bounds) - window?.rootViewController = ViewController() + window?.rootViewController = ScrollViewKeyboardViewController() window?.makeKeyAndVisible() return true diff --git a/SampleApp/Sources/ScrollViewKeyboardViewController.swift b/SampleApp/Sources/ScrollViewKeyboardViewController.swift new file mode 100644 index 000000000..c85978d2a --- /dev/null +++ b/SampleApp/Sources/ScrollViewKeyboardViewController.swift @@ -0,0 +1,49 @@ +// +// ScrollViewKeyboardViewController.swift +// SampleApp +// +// Created by Kyle Van Essen on 3/22/20. +// Copyright © 2020 Square. All rights reserved. +// + +import UIKit +import BlueprintUI +import BlueprintUICommonControls + + +final class ScrollViewKeyboardViewController : UIViewController +{ + override func loadView() { + + let view = BlueprintView() + + view.element = self.content() + + self.view = view + } + + private func content() -> Element + { + var scrollView = ScrollView(wrapping: Column { + $0.horizontalAlignment = .fill + + for _ in 1...20 { + let textField = TextField(text: "Hello") + + let box = Box( + backgroundColor: .init(white: 0.95, alpha: 1.0), + cornerStyle: .square, + wrapping: Inset(uniformInset: 20.0, wrapping: textField) + ) + + $0.add(child: box) + } + }) + + //scrollView.contentInset.bottom = 20.0 + + scrollView.keyboardAdjustmentMode = .adjustsWhenVisible + + return scrollView + } +} From 8dcd5d845eec9dbc83a337b7f1c78d46971da752 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Mon, 30 Mar 2020 15:36:46 -0700 Subject: [PATCH 2/4] Revert BlueprintUICommonControls.podspec testspec --- BlueprintUICommonControls.podspec | 5 ----- SampleApp/Podfile.lock | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/BlueprintUICommonControls.podspec b/BlueprintUICommonControls.podspec index 3cc7a9474..0cef463c8 100644 --- a/BlueprintUICommonControls.podspec +++ b/BlueprintUICommonControls.podspec @@ -14,9 +14,4 @@ Pod::Spec.new do |s| s.source_files = 'BlueprintUICommonControls/Sources/**/*.swift' s.dependency 'BlueprintUI' - - s.test_spec 'Tests' do |test_spec| - test_spec.source_files = 'BlueprintUICommonControls/Tests/**/*.swift' - test_spec.framework = 'XCTest' - end end diff --git a/SampleApp/Podfile.lock b/SampleApp/Podfile.lock index 2b7c82b7a..1887177a8 100644 --- a/SampleApp/Podfile.lock +++ b/SampleApp/Podfile.lock @@ -17,7 +17,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: BlueprintUI: 10f5373fdb6c03a8d8e04e67c50a4e3f5cafe8f3 - BlueprintUICommonControls: 8c283ad02b9d8b05d65d130e59f3efe6ad89a4c2 + BlueprintUICommonControls: 99dafabd80ba1bcd2e1a47ddd8cc43ef8561768c PODFILE CHECKSUM: 63720a1a50b146640cc4fcc4f36d3770895c7e0d From cea7aa6a06669e8dabbeff3e42f5bf8bfa22e45c Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Mon, 30 Mar 2020 17:53:35 -0700 Subject: [PATCH 3/4] Code review --- .../Sources/Internal/KeyboardObserver.swift | 29 ++++- .../Sources/ScrollView.swift | 26 ++--- .../Tests/ScrollViewTests.swift | 6 +- .../Internal/KeyboardObserverTests.swift | 110 +++++++----------- 4 files changed, 78 insertions(+), 93 deletions(-) diff --git a/BlueprintUICommonControls/Sources/Internal/KeyboardObserver.swift b/BlueprintUICommonControls/Sources/Internal/KeyboardObserver.swift index 3f4478344..939c73c8f 100644 --- a/BlueprintUICommonControls/Sources/Internal/KeyboardObserver.swift +++ b/BlueprintUICommonControls/Sources/Internal/KeyboardObserver.swift @@ -18,6 +18,25 @@ protocol KeyboardObserverDelegate : AnyObject { } /** + Encapsulates listening for system keyboard updates, plus transforming the visible frame of the keyboard into the coordinates of a requested view. + + You use this class by providing a delegate, which receives callbacks when changes to the keyboard frame occur. You would usually implement + the delegate somewhat like this: + + ``` + func keyboardFrameWillChange( + for observer : KeyboardObserver, + animationDuration : Double, + options : UIView.AnimationOptions + ) { + UIView.animate(withDuration: animationDuration, delay: 0.0, options: options, animations: { + // Use the frame from the keyboardObserver to update insets or sizing where relevant. + }) + } + ``` + + Notes + ----- Implementation borrowed from Listable: https://github.com/kyleve/Listable/blob/master/Listable/Sources/Internal/KeyboardObserver.swift @@ -61,7 +80,7 @@ final class KeyboardObserver { case nonOverlapping /// The current frame does overlap the view, by the provided rect, in the view's coordinate space. - case visible(frame: CGRect) + case overlapping(frame: CGRect) } /// How the keyboard overlaps the view provided. If the view is not on screen (eg, no window), @@ -79,7 +98,7 @@ final class KeyboardObserver { let frame = view.convert(notification.endingFrame, from: nil) if frame.intersects(view.bounds) { - return .visible(frame: frame) + return .overlapping(frame: frame) } else { return .nonOverlapping } @@ -97,10 +116,8 @@ final class KeyboardObserver { /// Only communicate a frame change to the delegate if the frame actually changed. - if let old = old { - guard old.endingFrame != new.endingFrame else { - return - } + if let old = old, old.endingFrame == new.endingFrame { + return } /** diff --git a/BlueprintUICommonControls/Sources/ScrollView.swift b/BlueprintUICommonControls/Sources/ScrollView.swift index efaeb31dd..9551815de 100644 --- a/BlueprintUICommonControls/Sources/ScrollView.swift +++ b/BlueprintUICommonControls/Sources/ScrollView.swift @@ -272,19 +272,14 @@ fileprivate final class ScrollerWrapperView: UIView { private func applyContentInset(with scrollView : ScrollView) { - // Keep a copy of the content inset as provided by the developer. - // We will use this when adjusting the `contentInset.bottom` for the keyboard. - - let contentInset = ScrollView.finalContentInset( + let contentInset = ScrollView.calculateContentInset( scrollViewInsets: scrollView.contentInset, safeAreaInsets: self.bp_safeAreaInsets, keyboardBottomInset: self.bottomContentInsetAdjustmentForKeyboard, refreshControlState: scrollView.pullToRefreshBehavior, refreshControlBounds: refreshControl?.bounds ) - - // Apply the updated contentInset if it changed. - + if self.scrollView.contentInset != contentInset { let wasScrolledToTop = self.scrollView.contentOffset.y == -self.scrollView.contentInset.top @@ -328,7 +323,7 @@ extension ScrollView { // Calculates the correct content inset to apply for the given inputs. - static func finalContentInset( + static func calculateContentInset( scrollViewInsets : UIEdgeInsets, safeAreaInsets : UIEdgeInsets, keyboardBottomInset : CGFloat, @@ -369,7 +364,7 @@ extension ScrollerWrapperView : KeyboardObserverDelegate { private func updateBottomContentInsetWithKeyboardFrame() { - let contentInset = ScrollView.finalContentInset( + let contentInset = ScrollView.calculateContentInset( scrollViewInsets: self.representedElement.contentInset, safeAreaInsets: self.bp_safeAreaInsets, keyboardBottomInset: self.bottomContentInsetAdjustmentForKeyboard, @@ -396,15 +391,10 @@ extension ScrollerWrapperView : KeyboardObserverDelegate { return 0.0 } - return { - switch keyboardFrame { - case .nonOverlapping: - return 0.0 - - case .visible(let frame): - return self.bounds.size.height - frame.origin.y - } - }() + switch keyboardFrame { + case .nonOverlapping: return 0.0 + case .overlapping(let frame): return self.bounds.size.height - frame.origin.y + } } } diff --git a/BlueprintUICommonControls/Tests/ScrollViewTests.swift b/BlueprintUICommonControls/Tests/ScrollViewTests.swift index fcd7be016..149b051c9 100644 --- a/BlueprintUICommonControls/Tests/ScrollViewTests.swift +++ b/BlueprintUICommonControls/Tests/ScrollViewTests.swift @@ -27,7 +27,7 @@ class ScrollViewTests : XCTestCase { XCTAssertEqual( UIEdgeInsets.zero, - ScrollView.finalContentInset( + ScrollView.calculateContentInset( scrollViewInsets: .zero, safeAreaInsets: UIEdgeInsets(top: 10.0, left: 11.0, bottom: 12.0, right: 13.0), keyboardBottomInset: .zero, @@ -41,7 +41,7 @@ class ScrollViewTests : XCTestCase { XCTAssertEqual( UIEdgeInsets(top: 10.0, left: 11.0, bottom: 50.0, right: 13.0), - ScrollView.finalContentInset( + ScrollView.calculateContentInset( scrollViewInsets: UIEdgeInsets(top: 10.0, left: 11.0, bottom: 12.0, right: 13.0), safeAreaInsets: UIEdgeInsets(top: 10.0, left: 11.0, bottom: 12.0, right: 13.0), keyboardBottomInset: 50.0, @@ -55,7 +55,7 @@ class ScrollViewTests : XCTestCase { XCTAssertEqual( UIEdgeInsets(top: 35.0, left: 11.0, bottom:50.0, right: 13.0), - ScrollView.finalContentInset( + ScrollView.calculateContentInset( scrollViewInsets: UIEdgeInsets(top: 10.0, left: 11.0, bottom: 12.0, right: 13.0), safeAreaInsets: UIEdgeInsets(top: 10.0, left: 11.0, bottom: 12.0, right: 13.0), keyboardBottomInset: 50.0, diff --git a/BlueprintUICommonControls/Tests/Sources/Internal/KeyboardObserverTests.swift b/BlueprintUICommonControls/Tests/Sources/Internal/KeyboardObserverTests.swift index a746a091d..a0f723cd6 100644 --- a/BlueprintUICommonControls/Tests/Sources/Internal/KeyboardObserverTests.swift +++ b/BlueprintUICommonControls/Tests/Sources/Internal/KeyboardObserverTests.swift @@ -8,8 +8,9 @@ class KeyboardObserverTests: XCTestCase { func test_notifications() { let center = NotificationCenter() - - self.testcase("Will Change Frame") { + + // Will Change Frame + do { let observer = KeyboardObserver(center: center) let delegate = Delegate() @@ -26,7 +27,8 @@ class KeyboardObserverTests: XCTestCase { XCTAssertEqual(delegate.keyboardFrameWillChange_callCount, 1) } - self.testcase("Did Change Frame") { + // Did Change Frame + do { let observer = KeyboardObserver(center: center) let delegate = Delegate() @@ -43,25 +45,26 @@ class KeyboardObserverTests: XCTestCase { XCTAssertEqual(delegate.keyboardFrameWillChange_callCount, 1) } - self.testcase("Only calls delegate for changed frame") { + // Only calls delegate for changed frame + do { let observer = KeyboardObserver(center: center) - let delegate = Delegate() - observer.delegate = delegate - - let userInfo : [AnyHashable:Any] = [ - UIResponder.keyboardFrameEndUserInfoKey : NSValue(cgRect: CGRect(x: 10.0, y: 10.0, width: 100.0, height: 200.0)), - UIResponder.keyboardAnimationDurationUserInfoKey : NSNumber(value: 2.5), - UIResponder.keyboardAnimationCurveUserInfoKey : NSNumber(value: 123) - ] - - XCTAssertEqual(delegate.keyboardFrameWillChange_callCount, 0) - center.post(Notification(name: UIWindow.keyboardDidChangeFrameNotification, object: nil, userInfo: userInfo)) - XCTAssertEqual(delegate.keyboardFrameWillChange_callCount, 1) - - XCTAssertEqual(delegate.keyboardFrameWillChange_callCount, 1) - center.post(Notification(name: UIWindow.keyboardDidChangeFrameNotification, object: nil, userInfo: userInfo)) - XCTAssertEqual(delegate.keyboardFrameWillChange_callCount, 1) + let delegate = Delegate() + observer.delegate = delegate + + let userInfo : [AnyHashable:Any] = [ + UIResponder.keyboardFrameEndUserInfoKey : NSValue(cgRect: CGRect(x: 10.0, y: 10.0, width: 100.0, height: 200.0)), + UIResponder.keyboardAnimationDurationUserInfoKey : NSNumber(value: 2.5), + UIResponder.keyboardAnimationCurveUserInfoKey : NSNumber(value: 123) + ] + + XCTAssertEqual(delegate.keyboardFrameWillChange_callCount, 0) + center.post(Notification(name: UIWindow.keyboardDidChangeFrameNotification, object: nil, userInfo: userInfo)) + XCTAssertEqual(delegate.keyboardFrameWillChange_callCount, 1) + + XCTAssertEqual(delegate.keyboardFrameWillChange_callCount, 1) + center.post(Notification(name: UIWindow.keyboardDidChangeFrameNotification, object: nil, userInfo: userInfo)) + XCTAssertEqual(delegate.keyboardFrameWillChange_callCount, 1) } } @@ -87,7 +90,8 @@ class KeyboardObserver_NotificationInfo_Tests : XCTestCase { UIResponder.keyboardAnimationCurveUserInfoKey : NSNumber(value: 123) ] - self.testcase("Successful Init") { + // Successful Init + do { let info = try! KeyboardObserver.NotificationInfo( with: Notification(name: UIResponder.keyboardDidShowNotification, object: nil, userInfo: defaultUserInfo) ) @@ -97,48 +101,49 @@ class KeyboardObserver_NotificationInfo_Tests : XCTestCase { XCTAssertEqual(info.animationCurve, 123) } - self.testcase("Failed Inits") { - - self.testcase("No userInfo") { - self.assertThrowsError(test: { + // Failed Inits + do { + // No userInfo + do { + XCTAssertThrowsError( try _ = KeyboardObserver.NotificationInfo( with: Notification(name: UIResponder.keyboardDidShowNotification, object: nil, userInfo: nil) ) - }, verify: { error in + ) { error in XCTAssertEqual(error as? KeyboardObserver.NotificationInfo.ParseError, .missingUserInfo) - }) + } } - self.testcase("No end frame") { - + // No end frame + do { var userInfo = defaultUserInfo userInfo.removeValue(forKey: UIResponder.keyboardFrameEndUserInfoKey) - self.assertThrowsError(test: { + XCTAssertThrowsError( try _ = KeyboardObserver.NotificationInfo( with: Notification(name: UIResponder.keyboardDidShowNotification, object: nil, userInfo: userInfo) ) - }, verify: { error in + ) { error in XCTAssertEqual(error as? KeyboardObserver.NotificationInfo.ParseError, .missingEndingFrame) - }) + } } - self.testcase("No animation duration") { - + // No animation duration + do { var userInfo = defaultUserInfo userInfo.removeValue(forKey: UIResponder.keyboardAnimationDurationUserInfoKey) - self.assertThrowsError(test: { + XCTAssertThrowsError( try _ = KeyboardObserver.NotificationInfo( with: Notification(name: UIResponder.keyboardDidShowNotification, object: nil, userInfo: userInfo) ) - }, verify: { error in + ) { error in XCTAssertEqual(error as? KeyboardObserver.NotificationInfo.ParseError, .missingAnimationDuration) - }) + } } - self.testcase("No animation curve") { - + // No animation curve + do { var userInfo = defaultUserInfo userInfo.removeValue(forKey: UIResponder.keyboardAnimationCurveUserInfoKey) @@ -150,33 +155,6 @@ class KeyboardObserver_NotificationInfo_Tests : XCTestCase { XCTAssertEqual(error as? KeyboardObserver.NotificationInfo.ParseError, .missingAnimationCurve) } } - } } } - - -extension XCTestCase { - - func testcase(_ name : String = "", _ block : () throws -> ()) { - do { - try block() - } catch { - XCTFail("Unexpected error thrown: \(error)") - } - } - - func assertThrowsError(test : () throws -> (), verify : (Error) -> ()) { - - var thrown = false - - do { - try test() - } catch { - thrown = true - verify(error) - } - - XCTAssertTrue(thrown, "Expected an error to be thrown but one was not.") - } -} From 95633fd812cfc390a8d3ec86ee91f6fa5381331f Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Mon, 30 Mar 2020 17:55:02 -0700 Subject: [PATCH 4/4] Code review fixes --- BlueprintUICommonControls/Tests/ScrollViewTests.swift | 7 +------ SampleApp/Sources/AppDelegate.swift | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/BlueprintUICommonControls/Tests/ScrollViewTests.swift b/BlueprintUICommonControls/Tests/ScrollViewTests.swift index 149b051c9..917f888ec 100644 --- a/BlueprintUICommonControls/Tests/ScrollViewTests.swift +++ b/BlueprintUICommonControls/Tests/ScrollViewTests.swift @@ -15,12 +15,7 @@ import BlueprintUI class ScrollViewTests : XCTestCase { - func test_contentInset() { - - let view = BlueprintView() - } - - func test_finalContentInset() + func test_calculateContentInset() { // No inset diff --git a/SampleApp/Sources/AppDelegate.swift b/SampleApp/Sources/AppDelegate.swift index 3334af057..b86166126 100644 --- a/SampleApp/Sources/AppDelegate.swift +++ b/SampleApp/Sources/AppDelegate.swift @@ -8,7 +8,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) - window?.rootViewController = ScrollViewKeyboardViewController() + window?.rootViewController = ViewController() window?.makeKeyAndVisible() return true