Keyboard offset no longer functions in SwiftUI 2 - swift

So I have an ObservableObject that sets a published variable 'currentHeight' to the height of the keyboard:
import Foundation
import SwiftUI
class KeyboardResponder: ObservableObject {
#Published var currentHeight: CGFloat = 0
var _center: NotificationCenter
init(center: NotificationCenter = .default) {
_center = center
_center.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
_center.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
#objc func keyBoardWillShow(notification: Notification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
withAnimation {
currentHeight = keyboardSize.height
}
}
print("the KEYBOARD HEIGHT IS \(self.currentHeight)")
}
#objc func keyBoardWillHide(notification: Notification) {
withAnimation {
currentHeight = 0
}
print("the KEYBOARD HEIGHT IS \(self.currentHeight)")
}
}
In my other views, I create an ObservedObject keyboardResponder and then inside the body of the view, I'll have for example some view where I set the vertical offset:
struct ViewName: View {
#ObservedObject var keyboardResponder = KeyboardResponder()
var body: some View {
GeometryReader { proxy in
VStack {
Text("this should be offset")
.offset(y: -keyboardResponder.currentHeight)
}.edgesIgnoringSafeArea(.all)
}
}
}
Before Xcode 12/SwiftUI 2 dropped, this worked like a charm but I think they changed something with how views refresh--anyone know what they changed and if there is a solution to my issue here?
EDIT: In my view, if I remove that edgesIgnoringSafeArea(.all), it sort of works, but weirdly. Essentially, the view can only move up to the point where all views are just in frame, it doesn't allow the entire view to shift up (and out of screen view)...I'll throw in a vid to show what I mean...
This is the link for when there are some more elements that take up most of the screen (so the offset is extremely small)
https://youtu.be/jpID11-rZDs
This is the link for when there are fewer elements, so the offset gets that much larger
https://youtu.be/MjqX1qDEA6E
If the GeometryReader is enclosing the view, that's what REALLY breaks it and stops it from responding at all. If I remove it, then it functions as needed...

As of the latest Swift, Xcode 12 and iOS14, I noticed that it is standard build in, when the textfield is not visible when typing. The screen is being risen with the keyboard height and shows the original field. Try this out with a scrollable view and 20 textfields for example.
Maybe you can get rid of your observableheight and do it without hard coding it.

Related

Swift iPad - How to present a formSheet with custom size based on a table view content?

In my app i present a form sheet in this way for iPad devices:
myViewController = .formSheet
present(myViewController, animated: true)
This ViewController is very simple: there is just a table view displaying n elements.
My goal is make the formSheet size the same as the table view with its n elements.
I actually achieved that with this code:
class MyViewControlller: UIViewController {
#IBOutlet weak var myTable: UITableView!
private var observer: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
myTable.delegate = self
myTable.dataSource = self
observer = myTable(\.contentSize, options: [.new], changeHandler: { [weak self] (table, _) in
guard let self = self else {
return
}
self.preferredContentSize = CGSize(width: table.contentSize.width, height: table.contentSize.height + 30.0)
self.view.layoutIfNeeded()
})
}
}
But the problem is that while presenting the formSheet (while it is sliding from bottom to the center of the screen) I can clearly see an animation where the bottom of the sheet is adjusting its size until it reaches the center of the screen.
What I'm trying to achieve is this: When the sheet starts sliding from the bottom to the center of the screen it already has the right dimensions.
Am I doing something wrong with the code?

Using UIScrollView correctly in SwiftUI

I wanted to implement pull to refresh for Scrollview of SwiftUi and so I tried to use the UIScrollView of UIKit to make use of its refreshControl. But I am getting a problem with the UIScrollview. There are several different views inside the scrollview and all of its data is fetched from network service using different API call and once the first api data is received the first view is shown inside the scrollview, similarly when second api data is received the second view is appended to the scrollview and similarly step by step all views are added to scrollview. Now in this process of step by step adding view, the scrollview doesn't get scrolled and its contents remain hiding above the navigationbar and below the bottom toolbar (may be the scrollView height takes full height of contents). But if i load the scrollView only after all the subview data is ready all subviews are loaded together then there is no issue. But i want to load the subviews as soon as i get the first data without waiting for all the data of every subviews to load completely.
The code that I have used is -
//UIScrollView
struct HomeScrollView: UIViewRepresentable {
private let uiScrollView: UIScrollView
init<Content: View>(#ViewBuilder content: () -> Content) {
let hosting = UIHostingController(rootView: content())
hosting.view.translatesAutoresizingMaskIntoConstraints = false
uiScrollView = UIScrollView()
uiScrollView.addSubview(hosting.view)
uiScrollView.showsVerticalScrollIndicator = false
uiScrollView.contentInsetAdjustmentBehavior = .never
let constraints = [
hosting.view.leadingAnchor.constraint(equalTo: uiScrollView.leadingAnchor),
hosting.view.trailingAnchor.constraint(equalTo: uiScrollView.trailingAnchor),
hosting.view.topAnchor.constraint(equalTo: uiScrollView.contentLayoutGuide.topAnchor, constant: 0),
hosting.view.bottomAnchor.constraint(equalTo: uiScrollView.contentLayoutGuide.bottomAnchor, constant: 0),
hosting.view.widthAnchor.constraint(equalTo: uiScrollView.widthAnchor)
]
uiScrollView.addConstraints(constraints)
}
func makeCoordinator() -> Coordinator {
Coordinator(legacyScrollView: self)
}
func makeUIView(context: Context) -> UIScrollView {
uiScrollView.refreshControl = UIRefreshControl()
uiScrollView.refreshControl?.addTarget(context.coordinator, action:
#selector(Coordinator.handleRefreshControl), for: .valueChanged)
return uiScrollView
}
func updateUIView(_ uiView: UIScrollView, context: Context) {}
class Coordinator: NSObject {
let legacyScrollView: HomeScrollView
init(legacyScrollView: HomeScrollView) {
self.legacyScrollView = legacyScrollView
}
#objc func handleRefreshControl(sender: UIRefreshControl) {
refreshData()
}
}
}
//SwiftUi inside view's body
var body: some View {
VStack {
HomeScrollView() {
ViewOne(data: modelDataOne)
ViewTwo(data: modelDataTwo)
ViewThree(data: modelDataThree)
...
}
}
}

SwiftUI: updateUIView() function in UIViewRepresentable causes app to freeze and CPU to spike

I have a custom Text Field in SwiftUI, that adjusts the number of lines it has according to the amount of content in the text field. In the app, the user can add text fields, and so I'm storing the heights, and content of the text fields in arrays, and appending to the arrays as more text fields are added.
Whenever I remove the code inside the updateUIView(), the app runs smoothly but of course, the text fields don't appear, but whenever I include it as in the code below, the CPU spikes to 99%, and the app freezes (even when there's only one text field).
Does anyone know why this is happening, and any fixes to it?
struct CustomMultilineTF: UIViewRepresentable {
#Binding var text: String
#Binding var height: CGFloat
var placeholder: String
func makeCoordinator() -> Coordinator {
return CustomMultilineTF.Coordinator(par: self)
}
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.isEditable = true
view.isScrollEnabled = true
view.text = placeholder
view.font = .systemFont(ofSize: 18)
view.textColor = UIColor.gray
view.delegate = context.coordinator
view.backgroundColor = UIColor.gray.withAlphaComponent(0.05)
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
DispatchQueue.main.async {
self.height = uiView.contentSize.height
}
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: CustomMultilineTF
init(par: CustomMultilineTF) {
parent = par
}
func textViewDidBeginEditing(_ textView: UITextView) {
if self.parent.text == "" {
textView.text = ""
textView.textColor = .black
}
}
func textViewDidChange(_ textView: UITextView) {
DispatchQueue.main.async {
self.parent.height = textView.contentSize.height
self.parent.text = textView.text
}
}
}
}
In your updateUIView, you're setting a value to self.height, which is a Binding. My guess is that the #Binding is connected to a property (either another #Binding or a #State on your surrounding view). So, whenever you set a new value to that #Binding, that triggers a refresh of the parent view. That, in turn, ends up calling updateUIView again, and you get into an infinite loop.
How to solve it probably depends on your architecture needs for the program. If you can get away with not having the parent know the height, you can probably solve it just by having the view update its own height.
You could also try only setting self.height to a new value if it != the old one -- that would probably short circuit the loop. But, you might end up with other side effects.

SwiftUI: Two-finger swipe ( scroll ) gesture

I'm interested in 2-finger swipe ( scroll ) gesture.
Not two-finger drag, but 2-finger swipe (without press). Like used in Safari to scroll up and down.
As I see noone of basic gestures will work for this: TapGesture - is not; LongPressGesture - not; DragGesture - not; MagnificationGesture - not; RotationGesture - not;
Have anyone some ideas how to do this?
I need at least direction to look at.
This is MacOS project
And by the way I cannot use UI classes in my project, I cannot re-made project to catalist
With due respect to #duncan-c 's answer, the more effective way is to use the NSResponder's scrollWheel(with: NSEvent) mechanism to track two-finger scrolling (one finger on the Apple Mouse).
However it's only available under NSView, so you need to integrate it into SwiftUI using NSRepresentableView.
Here is a complete set of working code that scrolls the main image using the scroll wheel. The code uses delegates and callbacks to pass the scroll event back up the chain into SwiftUI:
//
// ContentView.swift
// ScrollTest
//
// Created by TR Solutions on 6/9/21.
//
import SwiftUI
/// How the view passes events back to the representable view.
protocol ScrollViewDelegateProtocol {
/// Informs the receiver that the mouse’s scroll wheel has moved.
func scrollWheel(with event: NSEvent);
}
/// The AppKit view that captures scroll wheel events
class ScrollView: NSView {
/// Connection to the SwiftUI view that serves as the interface to our AppKit view.
var delegate: ScrollViewDelegateProtocol!
/// Let the responder chain know we will respond to events.
override var acceptsFirstResponder: Bool { true }
/// Informs the receiver that the mouse’s scroll wheel has moved.
override func scrollWheel(with event: NSEvent) {
// pass the event on to the delegate
delegate.scrollWheel(with: event)
}
}
/// The SwiftUI view that serves as the interface to our AppKit view.
struct RepresentableScrollView: NSViewRepresentable, ScrollViewDelegateProtocol {
/// The AppKit view our SwiftUI view manages.
typealias NSViewType = ScrollView
/// What the SwiftUI content wants us to do when the mouse's scroll wheel is moved.
private var scrollAction: ((NSEvent) -> Void)?
/// Creates the view object and configures its initial state.
func makeNSView(context: Context) -> ScrollView {
// Make a scroll view and become its delegate
let view = ScrollView()
view.delegate = self;
return view
}
/// Updates the state of the specified view with new information from SwiftUI.
func updateNSView(_ nsView: NSViewType, context: Context) {
}
/// Informs the representable view that the mouse’s scroll wheel has moved.
func scrollWheel(with event: NSEvent) {
// Do whatever the content view wants
// us to do when the scroll wheel moved
if let scrollAction = scrollAction {
scrollAction(event)
}
}
/// Modifier that allows the content view to set an action in its context.
func onScroll(_ action: #escaping (NSEvent) -> Void) -> Self {
var newSelf = self
newSelf.scrollAction = action
return newSelf
}
}
/// Our SwiftUI content view that we want to be able to scroll.
struct ContentView: View {
/// The scroll offset -- when this value changes the view will be redrawn.
#State var offset: CGSize = CGSize(width: 0.0, height: 0.0)
/// The SwiftUI view that detects the scroll wheel movement.
var scrollView: some View {
// A view that will update the offset state variable
// when the scroll wheel moves
RepresentableScrollView()
.onScroll { event in
offset = CGSize(width: offset.width + event.deltaX, height: offset.height + event.deltaY)
}
}
/// The body of our view.
var body: some View {
// What we want to be able to scroll using offset(),
// overlaid (must be on top or it can't get the scroll event!)
// with the view that tracks the scroll wheel.
Image(systemName:"applelogo")
.scaleEffect(20.0)
.frame(width: 200, height: 200, alignment: .center)
.offset(offset)
.overlay(scrollView)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Edit: Correcting my answer cover Mac OS
Scrolling up and down is a NSPanGestureRecognizer. That has a numberOfTouchesRequired property that lets you make it respond to 2 fingers if desired.
Mac OS does not have a swipe gesture recognizer.
The standard UISwipeGestureRecognizer does exactly what you want. Just set numberOfTouchesRequired to 2.
...Although I'm not sure mobile Safari uses swipe gestures. It might be a 2-finger drag with some special coding.
import Combine
#main
struct MyApp: App {
#State var subs = Set<AnyCancellable>() // Cancel onDisappear
#SceneBuilder
var body: some Scene {
WindowGroup {
SomeWindowView()
/////////////
// HERE!!!!!
/////////////
.onAppear { trackScrollWheel() }
}
}
}
/////////////
// HERE!!!!!
/////////////
extension MyApp {
func trackScrollWheel() {
NSApp.publisher(for: \.currentEvent)
.filter { event in event?.type == .scrollWheel }
.throttle(for: .milliseconds(200),
scheduler: DispatchQueue.main,
latest: true)
.sink {
if let event = $0 {
if event.deltaX > 0 { print("right") }
if event.deltaX < 0 { print("left") }
if event.deltaY > 0 { print("down") }
if event.deltaY < 0 { print("up") }
}
}
.store(in: &subs)
}
}

Dynamically adding to NSStackView and Adding Scrolling Functionality

Like the title says, I am trying to dynamically add NSTextViews and NSImageViews to a NSStackView. First, I added a Scroll View using IB and that made a hierarchy of Bordered Scroll View -> Clip View -> Main View -> Stack View (Vertical). I want to be able to dynamically add views to the stack view and be able to scroll through them all. I used autolayout on the Bordered Scroll View.
The problem: I tested this by adding 100 NSTextViews and incrementing their y-positions to stack up on each other. But, I cannot scroll to the top. I have been trying to understand this for a couple days but I just cannot figure it out. I cannot scroll and see all the text views, but when I increase the window size I can see more of them. TIA! `
import Cocoa
class ViewController: NSViewController {
var xLoc = CGFloat(0)
var yLoc = CGFloat(0)
#IBOutlet weak var mainView: NSStackView!
#IBOutlet weak var belowMainView: NSStackView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
for index in 1...100 {
var txt = "Num: \(index)"
setup(text: txt)
}
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
func setup(text: String){
let tempTextView = NSTextView(frame: NSMakeRect(xLoc, yLoc, 320, 10))
tempTextView.append(text)
tempTextView.isEditable = false
tempTextView.translatesAutoresizingMaskIntoConstraints = false
// mainView.addSubview(tempTextView)
belowMainView.addSubview(tempTextView)
yLoc += 20
}
}
// Adding an append function to textView functionality
extension NSTextView {
func append(_ string: String) {
self.textStorage?.append(NSAttributedString(string: string))
// Scrolls to end of document if it is out of view
self.scrollToEndOfDocument(nil)
}
}`