Force a custom gesture to end in SwiftUI - swiftui

I've implemented a custom gesture via the Gesture protocol and it is mostly working. The one thing I have yet to figure out is how to declare that the gesture has completed.
Here is my current code.
import Foundation
import SwiftUI
import CoreGraphics
struct WiggleGesture: Gesture {
struct WiggleState {
var startTime: Date?
var bounces: Int = 0
var lastLocation = CGPoint.zero
var lastBounceLocation = CGPoint.zero
}
struct WiggleValue {
var bounces: Int
var elapsedTime: TimeInterval?
}
typealias Value = WiggleValue
#GestureState var wiggleState = WiggleState()
let distanceThreshold: CGFloat
let angleThreshold: CGFloat
init(distanceThreshold: CGFloat = 15, angleThreshold: CGFloat = 120) {
self.distanceThreshold = distanceThreshold
self.angleThreshold = angleThreshold
}
var body: AnyGesture<Value> {
AnyGesture (
DragGesture(minimumDistance: distanceThreshold, coordinateSpace: .global)
.updating($wiggleState) { (value, state, _) in
if state.startTime == nil {
state.startTime = Date.now
}
let currentSwipe = value.translation.asVector() - state.lastBounceLocation.asVector() // The current broad motion, from the last bounce location.
let currentTrajectory = value.translation.asVector() - state.lastLocation.asVector() // The current immediate motion, from the last tick.
//TODO: Consider if there should be a minimum translation before counting a bounce?
let trajectoryDelta = abs(currentSwipe.angleTo(other: currentTrajectory))
if abs(trajectoryDelta) > angleThreshold {
state.bounces += 1
state.lastBounceLocation = value.translation.asPoint()
}
state.lastLocation = value.translation.asPoint()
}
.map { _ in
var elapsedTime: TimeInterval?
if let startTime = wiggleState.startTime {
elapsedTime = Date.now.timeIntervalSince(startTime)
}
return WiggleValue(bounces: wiggleState.bounces, elapsedTime: elapsedTime)
}
)
}
}
extension View {
func onWiggle(distanceThreshold: CGFloat = 15, angleThreshold: CGFloat = 120, perform action: #escaping (WiggleGesture.Value) -> Void) -> some View {
gesture(
WiggleGesture(distanceThreshold: distanceThreshold, angleThreshold: angleThreshold)
.onEnded { value in
action(value)
}
)
}
}
Currently, .onEnded is still the one that comes from DragGesture and triggers when the user releases their touch.
Ideally, what I want is to specify a "bounces required" and "timeout" to the gesture so that when it has detected n number of bounces within the timeout it will trigger the .onEnded call and the gesture will stop evaluating.

Related

Update #StateObject every time that init() is invoked in SwiftUI

I'm writing a SwiftUI code for a carousel, and I have a CarouselModel class that goes like this:
CarouselModel.swift
import Foundation
import SwiftUI
class CarouselModel: ObservableObject {
//MARK: - Properties
#Published private var imagesCount: Int
#Published private var currentImage: Int
#Published private var offsetMainAxis: CGFloat
private var screenSize: CGFloat
private var isHorizontal: Bool
private var padding: CGFloat
private var nextImage: Int {
return (self.currentImage+1) % self.imagesCount
}
private var prevImage: Int {
return (self.currentImage-1+self.imagesCount) % self.imagesCount
}
private var centralIndex: CGFloat {
return CGFloat(self.imagesCount-1)/2
}
public var computedOffset: CGFloat {
return (centralIndex-CGFloat(currentImage))*(screenSize-padding) + offsetMainAxis
}
init(imagesCount: Int, isHorizontal: Bool = true, padding: CGFloat = 20, currentImage: Int = 0) {
self.imagesCount = imagesCount
self.currentImage = currentImage
self.offsetMainAxis = 0
self.padding = padding
self.isHorizontal = isHorizontal
if isHorizontal {
self.screenSize = UIScreen.main.bounds.width
} else {
self.screenSize = UIScreen.main.bounds.height
}
}
//MARK: - Methods
public func getCurrentImage() -> Int {
return self.currentImage
}
public func goNext() -> Void {
withAnimation(.easeOut(duration: 0.5)) {
currentImage = nextImage
}
}
public func goPrevious() -> Void {
withAnimation(.easeOut(duration: 0.5)) {
currentImage = prevImage
}
}
public func onDragChange(offset: CGFloat) -> Void {
self.offsetMainAxis = offset
}
public func onDragEnd() -> Void {
if abs(offsetMainAxis) > 0.2*screenSize {
if offsetMainAxis > 0 {
withAnimation(.easeOut(duration: 0.5)) {
offsetMainAxis = 0
currentImage = prevImage
}
} else {
withAnimation(.easeOut(duration: 0.5)) {
offsetMainAxis = 0
currentImage = nextImage
}
}
} else {
withAnimation(.easeOut(duration: 0.5)) {
offsetMainAxis = 0
}
}
}
public func skipTo(number i: Int) {
withAnimation(.easeOut(duration: 0.5)) {
currentImage = i
}
}
}
At this point every time I need to place a concrete Carousel in my app I create a CarouselView.swift file containing some View, and the thing goes more or less like this:
CarouselView.swift
import SwiftUI
struct Carousel: View {
#StateObject var carousel: CarouselModel
private var screenWidth: CGFloat = UIScreen.main.bounds.width
private var images: [String]
private let padding: CGFloat
private var descriptions: [String]?
#State private var imageSelected: [String:Bool]
init(images: [String], descriptions: [String]?, padding: CGFloat = 20) {
self.images = images
self.padding = padding
_carousel = StateObject(
wrappedValue:
CarouselModel(
imagesCount: images.count,
isHorizontal: true,
padding: padding
)
)
self.imageSelected = Dictionary(uniqueKeysWithValues: images.map{ ($0, false) })
guard let desc = descriptions else {
self.descriptions = nil
return
}
self.descriptions = desc
}
var body: some View {
// USE carousel TO DISPLAY SOME CAROUSEL IMPLEMENTATION
}
}
Now, a problem arises when inside some container I need to display a different set of images based on a #State private var selectedImgSet: Int and each set has a different amount of pictures in it. The point is, whenever I call CarouselView(...) inside the container and its parameters change, its init is invoked but the CarouselModel doesn't get updated with the new images count.
This is totally expected since it is a #StateObject but I'm not sure how the update should happen. I can't use the .onAppear(perform: ) hook in the CarouselView since that is executed only once, when the CarouselView first appears.
What is the correct way to update the carousel: CarouselModel variable whenever CarouselView's init parameters change?
EDIT: Here are pastes for a full working examples: ContentView, CarouselItem, CarouselModel, CarouselView, Items. Add the following images sets to your assets: item00, item01, item02, item10, item11, item20, item21, item22, item23. Any 1920x1080p placeholder will do the trick for now.
I eventually fixed my issue the following way: I introduced the following method in CarouselModel:
public func update(_ images: Int) {
print("Performing update with \(images) images")
self.imagesCount = images
self.currentImage = 0
self.offsetMainAxis = 0
}
Then I added to the outmost View inside CarouselView the following modifier:
.onChange(of: self.images) { v in
self.carousel.update(v.count)
}
Now the update happens as expected.
You've got things the wrong way round. Create the #StateObject in the View above Carousel View and pass it in as #ObservedObject. However, since your model does no loading or saving it doesn't need to be a reference type, i.e. an object. A value type will suffice, i.e. a struct, so it can be #State in the parent and pass in as #Binding so you can call its mutating functions.

SwiftUI: Place and drag object immediately

I am attempting to place an object (a view of a square) on the screen and then drag it immediately. What I have achieved is the following:
I can drag existing objects that are already on the screen.
I can place new objects on the screen, but they do not drag along immediately. I need to lift my finger, and then tap on them again in order to drag.
How can I achieve the functionality: Place object on screen and immediately start dragging? I am missing something here. My code:
ContentView with square placement logic:
struct ContentView: View {
let screenSize = UIScreen.main.bounds
#State private var squares: [SquareView] = []
#State private var offsets = [CGSize](repeating: .zero, count: 300)
var body: some View {
GeometryReader { geo in
ForEach(squares, id: \.self) { square in
square
.position(x: square.startXLocation, y: square.startYLocation)
}
.ignoresSafeArea()
}
.onTouch(perform: updateLocation)
.onAppear {
for i in 0...2 {
let xLocation = Double.random(in: 50...(screenSize.width - 150))
let yLocation = Double.random(in: 50...(screenSize.height - 150))
let square = SquareView(sideLength: 40, number: i, startXLocation: xLocation, startYLocation: yLocation)
squares.append(square)
}
}
}
func updateLocation(_ location: CGPoint, type: TouchType) {
var square = SquareView(sideLength: 50, number: Int.random(in: 20...99), startXLocation: location.x, startYLocation: location.y)
if type == .started {
squares.append(square)
square.startXLocation = location.x
square.startYLocation = location.y
}
if type == .moved {
let newSquare = squares.last!
newSquare.offset = CGSize(width: location.x - newSquare.startXLocation, height: location.y - newSquare.startYLocation)
}
if type == .ended {
// Don't need to do anything here
}
}
}
The squares that I place with the logic to drag on the screen:
struct SquareView: View, Hashable {
let colors: [Color] = [.green, .red, .blue, .yellow]
let sideLength: Double
let number: Int
var startXLocation: Double
var startYLocation: Double
#State private var squareColor: Color = .yellow
#State var startOffset: CGSize = .zero
#State var offset: CGSize = .zero
var body: some View {
ZStack{
Rectangle()
.frame(width: sideLength, height: sideLength)
.foregroundColor(squareColor)
.onAppear {
squareColor = colors.randomElement()!
}
Text("\(number)")
} // ZStack
.offset(offset)
.gesture(
DragGesture()
.onChanged { gesture in
offset.width = gesture.translation.width + startOffset.width
offset.height = gesture.translation.height + startOffset.height
}
.onEnded { value in
startOffset.width = value.location.x
startOffset.height = value.location.y
}
)
}
static func ==(lhs: SquareView, rhs: SquareView) -> Bool {
return lhs.number == rhs.number
}
func hash(into hasher: inout Hasher) {
hasher.combine(number)
}
}
The struct used to detect the touch location on the screen (not relevant for the actual question, but necessary to reconstruct the program). Adapted from code by Paul Hudson, hackingwithswift.com:
// The types of touches users want to be notified about
struct TouchType: OptionSet {
let rawValue: Int
static let started = TouchType(rawValue: 1 << 0)
static let moved = TouchType(rawValue: 1 << 1)
static let ended = TouchType(rawValue: 1 << 2)
static let all: TouchType = [.started, .moved, .ended]
}
// Our UIKit to SwiftUI wrapper view
struct TouchLocatingView: UIViewRepresentable {
// A closer to call when touch data has arrived
var onUpdate: (CGPoint, TouchType) -> Void
// The list of touch types to be notified of
var types = TouchType.all
// Whether touch information should continue after the user's finger has left the view
var limitToBounds = true
func makeUIView(context: Context) -> TouchLocatingUIView {
// Create the underlying UIView, passing in our configuration
let view = TouchLocatingUIView()
view.onUpdate = onUpdate
view.touchTypes = types
view.limitToBounds = limitToBounds
return view
}
func updateUIView(_ uiView: TouchLocatingUIView, context: Context) {
}
// The internal UIView responsible for catching taps
class TouchLocatingUIView: UIView {
// Internal copies of our settings
var onUpdate: ((CGPoint, TouchType) -> Void)?
var touchTypes: TouchType = .all
var limitToBounds = true
// Our main initializer, making sure interaction is enabled.
override init(frame: CGRect) {
super.init(frame: frame)
isUserInteractionEnabled = true
}
// Just in case you're using storyboards!
required init?(coder: NSCoder) {
super.init(coder: coder)
isUserInteractionEnabled = true
}
// Triggered when a touch starts.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
send(location, forEvent: .started)
}
// Triggered when an existing touch moves.
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
send(location, forEvent: .moved)
}
// Triggered when the user lifts a finger.
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
send(location, forEvent: .ended)
}
// Triggered when the user's touch is interrupted, e.g. by a low battery alert.
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
send(location, forEvent: .ended)
}
// Send a touch location only if the user asked for it
func send(_ location: CGPoint, forEvent event: TouchType) {
guard touchTypes.contains(event) else {
return
}
if limitToBounds == false || bounds.contains(location) {
onUpdate?(CGPoint(x: round(location.x), y: round(location.y)), event)
}
}
}
}
// A custom SwiftUI view modifier that overlays a view with our UIView subclass.
struct TouchLocater: ViewModifier {
var type: TouchType = .all
var limitToBounds = true
let perform: (CGPoint, TouchType) -> Void
func body(content: Content) -> some View {
content
.background(
TouchLocatingView(onUpdate: perform, types: type, limitToBounds: limitToBounds)
)
// .overlay(
// TouchLocatingView(onUpdate: perform, types: type, limitToBounds: limitToBounds)
// )
}
}
// A new method on View that makes it easier to apply our touch locater view.
extension View {
func onTouch(type: TouchType = .all, limitToBounds: Bool = true, perform: #escaping (CGPoint, TouchType) -> Void) -> some View {
self.modifier(TouchLocater(type: type, limitToBounds: limitToBounds, perform: perform))
}
}
// Finally, here's some example code you can try out.
struct ContentView1: View {
var body: some View {
VStack {
Text("This will track all touches, inside bounds only.")
.padding()
.background(.red)
.onTouch(perform: updateLocation)
Text("This will track all touches, ignoring bounds – you can start a touch inside, then carry on moving it outside.")
.padding()
.background(.blue)
.onTouch(limitToBounds: false, perform: updateLocation)
Text("This will track only starting touches, inside bounds only.")
.padding()
.background(.green)
.onTouch(type: .started, perform: updateLocation)
}
}
func updateLocation(_ location: CGPoint, type: TouchType) {
print(location, type)
}
}
A possible approach is to handle drag and creation in "area" (background container), while "item" views are just rendered at the place where needed.
Find below a simplified demo (used Xcode 13.2 / iOS 15.2), see also comments in code snapshot.
Note: tap detection in already "existed" item is an exercise for you.
extension CGPoint: Identifiable { // just a helper for demo
public var id: String { "\(x)-\(y)" }
}
struct TapAndDragDemo: View {
#State private var points: [CGPoint] = [] // << persistent
#State private var point: CGPoint? // << current
#GestureState private var dragState: CGSize = CGSize.zero
var body: some View {
Color.clear.overlay( // << area
Group {
ForEach(points) { // << stored `items`
Rectangle()
.frame(width: 24, height: 24)
.position(x: $0.x, y: $0.y)
}
if let curr = point { // << active `item`
Rectangle().fill(Color.red)
.frame(width: 24, height: 24)
.position(x: curr.x, y: curr.y)
}
}
)
.contentShape(Rectangle()) // << make area tappable
.gesture(DragGesture(minimumDistance: 0.0)
.updating($dragState) { drag, state, _ in
state = drag.translation
}
.onChanged {
point = $0.location // track drag current
}
.onEnded {
points.append($0.location) // push to stored
point = nil
}
)
}
}

SwiftUICharts are not redrawn when given new data

I am adding the possibility to swipe in order to update a barchart. What I want to show is statistics for different station. To view different station I want the user to be able to swipe between the stations. I can see that the swiping works and each time I swipe I get the correct data from my controller. The problem is that my view is not redrawn properly.
I found this guide, but cannot make it work.
Say I swipe right from station 0 with data [100, 100, 100] to station 2, the retrieved data from my controller is [0.0, 100.0, 0.0]. The view I have still is for [100, 100, 100]`.
The station number is correctly updated, so I suspect it needs some state somehow.
Here is the code:
import SwiftUI
import SwiftUICharts
struct DetailedResultsView: View {
#ObservedObject var viewModel: ViewModel = .init()
#State private var tabIndex: Int = 0
#State private var startPos: CGPoint = .zero
#State private var isSwiping = true
var body: some View {
VStack {
Text("Station \(viewModel.getStation() + 1)")
TabView(selection: $tabIndex) {
BarCharts(data: viewModel.getData(kLatestRounds: 10, station: viewModel.getStation()), disciplineName: viewModel.getName()).tabItem { Group {
Image(systemName: "chart.bar")
Text("Last 10 Sessions")
}}.tag(0)
}
}.gesture(DragGesture()
.onChanged { gesture in
if self.isSwiping {
self.startPos = gesture.location
self.isSwiping.toggle()
}
}
.onEnded { gesture in
if gesture.location.x - startPos.x > 10 {
viewModel.decrementStation()
}
if gesture.location.x - startPos.x < -10 {
viewModel.incrementStation()
}
}
)
}
}
struct BarCharts: View {
var data: [Double]
var title: String
init(data: [Double], disciplineName: String) {
self.data = data
title = disciplineName
print(data)
}
var body: some View {
VStack {
BarChartView(data: ChartData(points: self.data), title: self.title, style: Styles.barChartStyleOrangeLight, form: CGSize(width: 300, height: 400))
}
}
}
class ViewModel: ObservableObject {
#Published var station = 1
let controller = DetailedViewController()
var isPreview = false
func getData(kLatestRounds: Int, station: Int) -> [Double] {
if isPreview {
return [100.0, 100.0, 100.0]
} else {
let data = controller.getResults(kLatestRounds: kLatestRounds, station: station, fileName: userDataFile)
return data
}
}
func getName() -> String {
controller.getDiscipline().name
}
func getNumberOfStations() -> Int {
controller.getDiscipline().getNumberOfStations()
}
func getStation() -> Int {
station
}
func incrementStation() {
station = (station + 1) % getNumberOfStations()
}
func decrementStation() {
station -= 1
if station < 0 {
station = getNumberOfStations() - 1
}
}
}
The data is printed inside the constructor each time I swipe. Shouldn't that mean it should be updated?
I don’t use SwiftUICharts so I can’t test it, but the least you can try is manually set the id to the view
struct DetailedResultsView: View {
#ObservedObject var viewModel: ViewModel = .init()
#State private var tabIndex: Int = 0
#State private var startPos: CGPoint = .zero
#State private var isSwiping = true
var body: some View {
VStack {
Text("Station \(viewModel.getStation() + 1)")
TabView(selection: $tabIndex) {
BarCharts(data: viewModel.getData(kLatestRounds: 10, station: viewModel.getStation()), disciplineName: viewModel.getName())
.id(viewmodel.station) // here. If it doesn’t work, you can set it to the whole TabView
.tabItem { Group {
Image(systemName: "chart.bar")
Text("Last 10 Sessions")
}}.tag(0)
}
}.gesture(DragGesture()
.onChanged { gesture in
if self.isSwiping {
self.startPos = gesture.location
self.isSwiping.toggle()
}
}
.onEnded { gesture in
if gesture.location.x - startPos.x > 10 {
viewModel.decrementStation()
}
if gesture.location.x - startPos.x < -10 {
viewModel.incrementStation()
}
}
)
}
}

swiftUI Synchronous issue with running a function before I have calculated the data for the ZStack element

I am using SwiftUI but have a conceptual problem.
I have edited down my code for (hopefully to fully explain the issue )
I want to do some calculations after the button is pressed AFTER which I want to plot the graph.
I am using minX as the bogus result of calculations.
Firstly, note that I have a file (called myGlobal.swift) the contents of which are:
import Foundation
class myGlobalVariables: ObservableObject
{
#Published var minX :Double = 99999
}
The value of this in the getLineChartDataSet = 9999
(from the Global file)
Before I press the button
This changed after I press the button to 222.222 in Mohrs2DCalc func
But of course I have already run myLineChartSwiftUI before I pressed the button,
But want to run myLineChartSwiftUI only AFTER I have pressed the button
import SwiftUI
import Charts
struct PricipalStresses2D: View {
#ObservedObject var input = myGlobalVariables()
var body: some View
{
ScrollView(.vertical, showsIndicators: true)
{
VStack(spacing: -10)
{
Button(action: {
self.CheckInputs()
})
{
Text("Calculations Before Plot")
}.padding(50)
myLineChartSwiftUI() //use frame to change the graph size within your SwiftUI view
.frame(alignment: .center)
.aspectRatio(contentMode: .fit)
}
}
}
func CheckInputs()
{
//Assume all inputs are OK
self.Mohrs2DCalc()
}
func Mohrs2DCalc()
{
let minX = 222.222 // Just to set it as if we had calculated it
self.input.minX = minX
print("Mohrs circle minX = \(self.input.minX)")
}
struct myLineChartSwiftUI : UIViewRepresentable
{
let lineChart = LineChartView()
func makeUIView(context: UIViewRepresentableContext<myLineChartSwiftUI>) -> LineChartView {
setUpChart()
return lineChart
}
func updateUIView(_ uiView: LineChartView, context: UIViewRepresentableContext<myLineChartSwiftUI>) {
}
func setUpChart() {
//lineChart.noDataText = "No Data Available"
let dataSets = [getLineChartDataSet()]
let data = LineChartData(dataSets: dataSets)
//data.setValueFont(.systemFont(ofSize: 7, weight: .light))
lineChart.data = data
}
func getChartDataPoints(sessions: [Int], accuracy: [Double]) -> [ChartDataEntry] {
var dataPoints: [ChartDataEntry] = []
for count in (0..<sessions.count) {
dataPoints.append(ChartDataEntry.init(x: Double(sessions[count]), y: accuracy[count]))
}
return dataPoints
}
func getLineChartDataSet() -> LineChartDataSet {
print("In getLineChartDataSet, minX = ", myGlobalVariables.self.init().minX) // This works fine
let dataPoints = getChartDataPoints(sessions: [0,1], accuracy: [0.0,10.0]) // sessions = x, accuracy = y
let set = LineChartDataSet(entries: dataPoints, label: "DataSet")
set.lineWidth = 0
//set.circleRadius = 4
//set.circleHoleRadius = 2
let color = ChartColorTemplates.vordiplom()[4]
set.setColor(color)
//set.setCircleColor(color)
// Got to set the xMin and xMax from the global to obtain it here
return set
}
}
} //View
struct PricipalStresses2D_Previews: PreviewProvider {
static var previews: some View {
PricipalStresses2D()
}
}
Your help will be appreciated,
TIA, Phil.

How to make the bottom button follow the keyboard display in SwiftUI

With the help of the following, I was able to follow the button on the keyboard display.
However, animation cannot be applied well.
How to show complete List when keyboard is showing up in SwiftUI
import SwiftUI
import Combine
import UIKit
class KeyboardResponder: ObservableObject {
let willset = PassthroughSubject<CGFloat, Never>()
private var _center: NotificationCenter
#Published var currentHeight: CGFloat = 0
var keyboardDuration: TimeInterval = 0
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)
}
deinit {
_center.removeObserver(self)
}
#objc func keyBoardWillShow(notification: Notification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
currentHeight = keyboardSize.height
guard let duration:TimeInterval = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return }
keyboardDuration = duration
}
}
#objc func keyBoardWillHide(notification: Notification) {
currentHeight = 0
}
}
import SwiftUI
struct Content: View {
#ObservedObject var keyboard = KeyboardResponder()
var body: some View {
VStack {
Text("text")
Spacer()
NavigationLink(destination: SubContentView()) {
Text("Done")
}
}
.padding(.bottom, keyboard.currentHeight)
animation(Animation.easeInOut(duration: keyboard.keyboardDuration))
}
}
enter image description here
Your main problem, is that you are using an implicit animation. Not only it may be animating things you may not want to animate, but also, you should never apply .animation() on containers. Of the few warnings in SwiftUI's documentation, this is one of them:
Use this modifier on leaf views rather than container views. The
animation applies to all child views within this view; calling
animation(_:) on a container view can lead to unbounded scope.
Source: https://developer.apple.com/documentation/swiftui/view/3278508-animation
The modified code removes the implicit .animation() call and replaces it with two implicit withAnimation closures.
I also replaced keyboardFrameEndUserInfoKey with keyboardFrameEndUserInfoKey, second calls were giving useless geometry.
class KeyboardResponder: ObservableObject {
let willset = PassthroughSubject<CGFloat, Never>()
private var _center: NotificationCenter
#Published var currentHeight: CGFloat = 0
var keyboardDuration: TimeInterval = 0
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)
}
deinit {
_center.removeObserver(self)
}
#objc func keyBoardWillShow(notification: Notification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
guard let duration:TimeInterval = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return }
keyboardDuration = duration
withAnimation(.easeInOut(duration: duration)) {
self.currentHeight = keyboardSize.height
}
}
}
#objc func keyBoardWillHide(notification: Notification) {
guard let duration:TimeInterval = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return }
withAnimation(.easeInOut(duration: duration)) {
currentHeight = 0
}
}
}
struct ContentView: View {
#ObservedObject var keyboard = KeyboardResponder()
var body: some View {
return VStack {
Text("text \(keyboard.currentHeight)")
TextField("Enter text", text: .constant(""))
Spacer()
NavigationLink(destination: Text("SubContentView()")) {
Text("Done")
}
}
.padding(.bottom, keyboard.currentHeight)
// .animation(Animation.easeInOut(duration: keyboard.keyboardDuration))
}
}