diff --git a/BlueprintUICommonControls/Sources/Internal/KeyboardObserver.swift b/BlueprintUICommonControls/Sources/Internal/KeyboardObserver.swift new file mode 100644 index 000000000..939c73c8f --- /dev/null +++ b/BlueprintUICommonControls/Sources/Internal/KeyboardObserver.swift @@ -0,0 +1,195 @@ +// +// 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 + ) +} + +/** + 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 + + 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 overlapping(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 .overlapping(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, old.endingFrame == new.endingFrame { + 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 8146bdfb5..9551815de 100644 --- a/BlueprintUICommonControls/Sources/ScrollView.swift +++ b/BlueprintUICommonControls/Sources/ScrollView.swift @@ -12,12 +12,23 @@ 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 keyboardDismissMode: UIScrollView.KeyboardDismissMode = .none + public var keyboardAdjustmentMode : KeyboardAdjustmentMode = .adjustsWhenVisible + public init(wrapping element: Element) { self.wrappedElement = element @@ -29,10 +40,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) + } } } @@ -40,11 +56,9 @@ public struct ScrollView: Element { return Layout( contentInset: contentInset, contentSize: contentSize, - centersUnderflow: centersUnderflow) + centersUnderflow: centersUnderflow + ) } - - - } extension ScrollView { @@ -97,6 +111,11 @@ extension ScrollView { } extension ScrollView { + + public enum KeyboardAdjustmentMode : Equatable { + case none + case adjustsWhenVisible + } public enum ContentSize : Equatable { @@ -145,6 +164,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 { @@ -156,8 +179,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) } @@ -177,6 +206,8 @@ fileprivate final class ScrollerWrapperView: UIView { func apply(scrollView: ScrollView, contentFrame: CGRect) { + self.representedElement = scrollView + switch scrollView.pullToRefreshBehavior { case .disabled, .refreshing: refreshAction = { } @@ -231,20 +262,24 @@ fileprivate final class ScrollerWrapperView: UIView { if self.scrollView.showsHorizontalScrollIndicator != scrollView.showsHorizontalScrollIndicator { self.scrollView.showsHorizontalScrollIndicator = scrollView.showsHorizontalScrollIndicator } - + if self.scrollView.keyboardDismissMode != scrollView.keyboardDismissMode { self.scrollView.keyboardDismissMode = scrollView.keyboardDismissMode } - - 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 - } - + + self.applyContentInset(with: scrollView) + } + + private func applyContentInset(with scrollView : ScrollView) + { + let contentInset = ScrollView.calculateContentInset( + scrollViewInsets: scrollView.contentInset, + safeAreaInsets: self.bp_safeAreaInsets, + keyboardBottomInset: self.bottomContentInsetAdjustmentForKeyboard, + refreshControlState: scrollView.pullToRefreshBehavior, + refreshControlBounds: refreshControl?.bounds + ) + if self.scrollView.contentInset != contentInset { let wasScrolledToTop = self.scrollView.contentOffset.y == -self.scrollView.contentInset.top @@ -260,8 +295,132 @@ 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 calculateContentInset( + 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.calculateContentInset( + 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 + } + + switch keyboardFrame { + case .nonOverlapping: return 0.0 + case .overlapping(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..917f888ec --- /dev/null +++ b/BlueprintUICommonControls/Tests/ScrollViewTests.swift @@ -0,0 +1,62 @@ +// +// 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_calculateContentInset() + { + // No inset + + XCTAssertEqual( + UIEdgeInsets.zero, + + ScrollView.calculateContentInset( + 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.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, + 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.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, + 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..a0f723cd6 --- /dev/null +++ b/BlueprintUICommonControls/Tests/Sources/Internal/KeyboardObserverTests.swift @@ -0,0 +1,160 @@ +import XCTest +import UIKit + +@testable import BlueprintUICommonControls + + +class KeyboardObserverTests: XCTestCase { + + func test_notifications() { + let center = NotificationCenter() + + // Will Change 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.keyboardWillChangeFrameNotification, object: nil, userInfo: userInfo)) + XCTAssertEqual(delegate.keyboardFrameWillChange_callCount, 1) + } + + // Did Change 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) + } + + // 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) + } + } + + 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) + ] + + // Successful Init + do { + 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) + } + + // Failed Inits + do { + // No userInfo + do { + XCTAssertThrowsError( + try _ = KeyboardObserver.NotificationInfo( + with: Notification(name: UIResponder.keyboardDidShowNotification, object: nil, userInfo: nil) + ) + ) { error in + XCTAssertEqual(error as? KeyboardObserver.NotificationInfo.ParseError, .missingUserInfo) + } + } + + // No end frame + do { + var userInfo = defaultUserInfo + userInfo.removeValue(forKey: UIResponder.keyboardFrameEndUserInfoKey) + + XCTAssertThrowsError( + try _ = KeyboardObserver.NotificationInfo( + with: Notification(name: UIResponder.keyboardDidShowNotification, object: nil, userInfo: userInfo) + ) + ) { error in + XCTAssertEqual(error as? KeyboardObserver.NotificationInfo.ParseError, .missingEndingFrame) + } + } + + // No animation duration + do { + var userInfo = defaultUserInfo + userInfo.removeValue(forKey: UIResponder.keyboardAnimationDurationUserInfoKey) + + XCTAssertThrowsError( + try _ = KeyboardObserver.NotificationInfo( + with: Notification(name: UIResponder.keyboardDidShowNotification, object: nil, userInfo: userInfo) + ) + ) { error in + XCTAssertEqual(error as? KeyboardObserver.NotificationInfo.ParseError, .missingAnimationDuration) + } + } + + // No animation curve + do { + 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) + } + } + } + } +} diff --git a/SampleApp/Podfile b/SampleApp/Podfile index b6a6963c0..332925fd3 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' end target 'SampleApp' do diff --git a/SampleApp/Podfile.lock b/SampleApp/Podfile.lock index 924128a52..1887177a8 100644 --- a/SampleApp/Podfile.lock +++ b/SampleApp/Podfile.lock @@ -19,6 +19,6 @@ SPEC CHECKSUMS: BlueprintUI: 10f5373fdb6c03a8d8e04e67c50a4e3f5cafe8f3 BlueprintUICommonControls: 99dafabd80ba1bcd2e1a47ddd8cc43ef8561768c -PODFILE CHECKSUM: e9562cee84054ca8676daa6bfce31541238548f0 +PODFILE CHECKSUM: 63720a1a50b146640cc4fcc4f36d3770895c7e0d COCOAPODS: 1.9.1 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/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 + } +}