Hi! I was trying to add an animation to my SwiftUI view with UIKit, but after the animation runs there's a delay before the view will accept touch interactions. I thought it was because of the frame size of the view controller, but even after fixing that I still get the delay. Could anyone point me to where I might be going wrong, or if maybe using a UIKit modifier for the animation just doesn't work?
Any help would be greatly appreciated!
UIView:
class BounceView: UIView {
required init() {
super.init(frame: .zero)
}
func bounceAnimation() {
guard let piece = self.subviews.first else { return }
UIView.animate(withDuration: 0.7, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0) {
piece.frame.origin.x += 10
}
}
func bounceBack() {
guard let piece = self.subviews.first else { return }
UIView.animate(withDuration: 0.7, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0) {
piece.frame.origin.x -= 10
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
UIView controller:
class BounceViewController: UIViewController {
init(controller: UIViewController) {
super.init(nibName: nil, bundle: nil)
view = BounceView()
addChild(controller)
controller.view.translatesAutoresizingMaskIntoConstraints = false
controller.view.backgroundColor = .clear
view.addSubview(controller.view)
controller.didMove(toParent: self)
}
// adjusts view to match bounds of child
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let subviewFrame = self.view.subviews.first?.bounds ?? .zero
view.frame = subviewFrame
print(subviewFrame)
self.updateViewConstraints()
}
func update(animated: Bool) {
let bounceView = view as? BounceView
if animated {
bounceView?.bounceAnimation()
} else {
bounceView?.bounceBack()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
SwiftUI wrapper:
struct BounceUIViewController: UIViewControllerRepresentable {
private var controller: UIViewController
@Binding var animated: Bool
init(controller: UIViewController, animated: Binding<Bool>) {
self.controller = controller
self._animated = animated
}
func makeUIViewController(context: Context) -> BounceViewController {
BounceViewController(controller: controller)
}
func updateUIViewController(_ uiViewController: BounceViewController, context: Context) {
uiViewController.update(animated: animated)
}
}
View extension:
extension View {
func bounce(animated: Binding<Bool>) -> some View {
modifier(Bounce(animated: animated))
}
}
struct Bounce: ViewModifier {
@Binding var animated: Bool
init(animated: Binding<Bool>) {
self._animated = animated
}
func body(content: Content) -> some View {
BounceUIViewController(controller: content.uiViewController, animated: $animated)
}
}
Post
Replies
Boosts
Views
Activity
I'm trying to create a brush by drawing in CGContext using a UIImage for a brush. However, I when I try drawing, the stroke is antialiased when I don't want it to be. I tried
context.interpolationQuality = .none
context.setShouldAntialias(false)
but it doesn't seem to work. Is it a problem with my brush or resizing the brush maybe?? (Also this problem doesn't occur when I draw a regular stroke without the brush.)
Any help or advice would be greatly appreciated!
Here is my draw function
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else { return }
context.interpolationQuality = .none
context.setShouldAntialias(false)
for stroke in strokes {
let brush = UIColor.blue.circle(size: CGSize(width: CGFloat(stroke.width * 10), height: CGFloat(stroke.width * 10))).mask(color: UIColor(cgColor: stroke.color))
var first = true
for point in stroke.points {
if first {
first = false
context.move(to: point)
continue
}
context.addLine(to: point, using: brush)
}
}
}
Circle brush
extension UIColor {
func circle(size: CGSize = CGSize(width: 1, height: 1)) -> UIImage {
return UIGraphicsImageRenderer(size: size).image { rendererContext in
self.setFill()
UIBezierPath(ovalIn: CGRect(origin: .zero, size: size)).fill()
}
}
}
CGConext extension where I overloaded addLine
extension CGContext {
func addLine(to point: CGPoint, using brush: UIImage, density: CGFloat = 1.0) {
var frame: CGRect = .zero
frame.size = brush.size
let lastPoint = self.currentPointOfPath
let distanceX = point.x - lastPoint.x
let distanceY = point.y - lastPoint.y
let distanceR = sqrt(pow(distanceX, 2) + pow(distanceY, 2))
let deltaR = (1.0 / density)
let numOfSteps = ceil(distanceR / deltaR)
var renders : CGFloat = 0.0
let deltaX = distanceX / numOfSteps
let deltaY = distanceY / numOfSteps
var currentCenter = lastPoint
repeat {
frame.origin.x = currentCenter.x - frame.width / 2.0
frame.origin.y = currentCenter.y - frame.height / 2.0
brush.draw(in: frame)
currentCenter.x += deltaX
currentCenter.y += deltaY
renders += 1.0
} while (renders <= numOfSteps)
self.move(to: point)
}
}
I made a custom slider by subclassing UISlider, and I'm trying to add scrubbing functionality to it, but for some reason the scrubbing is barely even noticeable at 0.1? In my code, I tried multiplying change in x distance by the scrubbing value, but it doesn't seem to work. Also, when I manually set the scrubbing speed to a lower value such as 0.01, it does go slower but it looks really laggy and weird?? What am I doing wrong?
Any help or advice would be greatly appreciated!
Subclass of UISlider:
class SizeSliderView: UISlider {
private var previousLocation: CGPoint?
private var currentLocation: CGPoint?
private var translation: CGFloat = 0
private var scrubbingSpeed: CGFloat = 1
private var defaultDiameter: Float
init(startValue: Float = 0, defaultDiameter: Float = 500) {
self.defaultDiameter = defaultDiameter
super.init(frame: .zero)
value = clamp(value: startValue, min: minimumValue, max: maximumValue)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
super.draw(rect)
clear()
createThumbImageView()
addTarget(self, action: #selector(valueChanged(_:)), for: .valueChanged)
}
// Clear elements
private func clear() {
tintColor = .clear
maximumTrackTintColor = .clear
backgroundColor = .clear
thumbTintColor = .clear
}
// Call when value is changed
@objc private func valueChanged(_ sender: SizeSliderView) {
CATransaction.begin()
CATransaction.setDisableActions(true)
CATransaction.commit()
createThumbImageView()
}
// Create thumb image with thumb diameter dependent on thumb value
private func createThumbImageView() {
let thumbDiameter = CGFloat(defaultDiameter * value)
let thumbImage = UIColor.red.circle(CGSize(width: thumbDiameter, height: thumbDiameter))
setThumbImage(thumbImage, for: .normal)
setThumbImage(thumbImage, for: .highlighted)
setThumbImage(thumbImage, for: .application)
setThumbImage(thumbImage, for: .disabled)
setThumbImage(thumbImage, for: .focused)
setThumbImage(thumbImage, for: .reserved)
setThumbImage(thumbImage, for: .selected)
}
// Return true so touches are tracked
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let location = touch.location(in: self)
// Ensure that start location is on thumb
let thumbDiameter = CGFloat(defaultDiameter * value)
if location.x < bounds.width / 2 - thumbDiameter / 2 || location.x > bounds.width / 2 + thumbDiameter / 2 || location.y < 0 || location.y > thumbDiameter {
return false
}
previousLocation = location
super.beginTracking(touch, with: event)
return true
}
// Track based on moving slider
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
guard isTracking else { return false }
guard let previousLocation = previousLocation else { return false }
// Reference
// location: location of touch relative to device
// delta location: change in touch location WITH scrubbing
// adjusted location: location of touch to slider bounds (WITH scrubbing)
// translation: location of slider relative to device
let location = touch.location(in: self)
currentLocation = location
scrubbingSpeed = getScrubbingSpeed(for: location.y - 50)
let deltaLocation = (location.x - previousLocation.x) * scrubbingSpeed
var adjustedLocation = deltaLocation + previousLocation.x - translation
if adjustedLocation < 0 {
translation += adjustedLocation
adjustedLocation = deltaLocation + previousLocation.x - translation
} else if adjustedLocation > bounds.width {
translation += adjustedLocation - bounds.width
adjustedLocation = deltaLocation + previousLocation.x - translation
}
self.previousLocation = CGPoint(x: deltaLocation + previousLocation.x, y: location.y)
let newValue = Float(adjustedLocation / bounds.width) * (maximumValue - minimumValue) + minimumValue
setValue(newValue, animated: false)
sendActions(for: .valueChanged)
return true
}
// Reset start and current location
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.currentLocation = nil
self.translation = 0
super.touchesEnded(touches, with: event)
}
// Thumb location follows current location and resets in middle
override func thumbRect(forBounds bounds: CGRect, trackRect rect: CGRect, value: Float) -> CGRect {
let thumbDiameter = CGFloat(defaultDiameter * value)
let origin = CGPoint(x: (currentLocation?.x ?? bounds.width / 2) - thumbDiameter / 2, y: (currentLocation?.y ?? thumbDiameter / 2) - thumbDiameter / 2)
return CGRect(origin: origin, size: CGSize(width: thumbDiameter, height: thumbDiameter))
}
private func getScrubbingSpeed(for value: CGFloat) -> CGFloat {
switch value {
case 0:
return 1
case 0...50:
return 0.5
case 50...100:
return 0.25
case 100...:
return 0.1
default:
return 1
}
}
private func clamp(value: Float, min: Float, max: Float) -> Float {
if value < min {
return min
} else if value > max {
return max
} else {
return value
}
}
}
UIView representative:
struct SizeSlider: UIViewRepresentable {
private var startValue: Float
private var defaultDiameter: Float
init(startValue: Float, defaultDiameter: Float) {
self.startValue = startValue
self.defaultDiameter = defaultDiameter
}
func makeUIView(context: Context) -> SizeSliderView {
let view = SizeSliderView(startValue: startValue, defaultDiameter: defaultDiameter)
view.minimumValue = 0.1
view.maximumValue = 1
return view
}
func updateUIView(_ uiView: SizeSliderView, context: Context) {}
}
Content view:
struct ContentView: View {
var body: some View {
SizeSlider(startValue: 0.20, defaultDiameter: 100)
.frame(width: 400)
}
}
I made an extension with a UIView that takes a SwiftUI view, gets its UIView, and then adds it as a subview. But now the subview isn't receiving any touches and idk how to fix that. I've already tried point(inside:with:) but it doesn't seem to work. I've also tried forwarding the touches from touchesBegan, touchesMoved, etc., but that didn't work either.
Any help or advice would be greatly appreciated!
Extension with the UIView:
// Add view modifier to View
extension View {
func transformable() -> some View {
modifier(Transformable())
}
}
// View modifier
struct Transformable: ViewModifier {
func body(content: Content) -> some View {
TransformableUIView(content: content)
}
}
// Wrap UIView
struct TransformableUIView<V>: UIViewRepresentable where V: View {
private var content: V
init(content: V) {
self.content = content
}
func makeUIView(context: Context) -> TransformableView<V> {
TransformableView(content: content)
}
func updateUIView(_ uiView: TransformableView<V>, context: Context) {}
}
// View that handles zoom, pan, and rotate gestures
class TransformableView<V>: UIView, UIGestureRecognizerDelegate where V: View {
private var content: V
private var initialCenter: CGPoint = .zero
private var totalScale: CGFloat = 1.0
private var boundsDidSet = false
required init(content: V) {
self.content = content
super.init(frame: .zero)
self.addSubview(content.uiView)
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panPiece(_:)))
panGesture.minimumNumberOfTouches = 2
panGesture.maximumNumberOfTouches = 2
panGesture.delegate = self
self.addGestureRecognizer(panGesture)
let scaleGesture = UIPinchGestureRecognizer(target: self, action: #selector(scalePiece(_:)))
scaleGesture.delegate = self
self.addGestureRecognizer(scaleGesture)
let rotateGesture = UIRotationGestureRecognizer(target: self, action: #selector(rotatePiece(_:)))
rotateGesture.delegate = self
self.addGestureRecognizer(rotateGesture)
}
// Position content in center of view
override func layoutSubviews() {
super.layoutSubviews()
// Return if bounds are already set
if boundsDidSet {
return
}
guard let piece = self.subviews.first else { return }
piece.center = CGPoint(x: self.bounds.size.width / 2, y: self.bounds.size.height / 2)
boundsDidSet = true
}
// Function called when pan gesture is recognized
@objc private func panPiece(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let piece = gestureRecognizer.view?.subviews.first else { return }
// Get the changes in the X and Y directions relative to
// the superview's coordinate space.
let translation = gestureRecognizer.translation(in: piece.superview)
if gestureRecognizer.state == .began {
// Save the view's original position.
self.initialCenter = piece.center
}
// Update the position for the .began, .changed, and .ended states
if gestureRecognizer.state != .cancelled {
// Add the X and Y translation to the view's original position.
var newCenter = CGPoint(x: initialCenter.x + translation.x, y: initialCenter.y + translation.y)
// Prevent content from leaving view
newCenter.x = clamp(value: newCenter.x, min: 0, max: self.bounds.width)
newCenter.y = clamp(value: newCenter.y, min: 0, max: self.bounds.height)
piece.center = newCenter
}
else {
// On cancellation, return the piece to its original location.
piece.center = initialCenter
}
}
// Function called when scale gesture is recognized
@objc private func scalePiece(_ gestureRecognizer : UIPinchGestureRecognizer) {
guard let piece = gestureRecognizer.view?.subviews.first else { return }
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
// Set min/max zoom
let newScale = clamp(value: totalScale * gestureRecognizer.scale, min: 0.2, max: 20) / totalScale
piece.transform = (piece.transform.scaledBy(x: newScale, y: newScale))
gestureRecognizer.scale = 1.0
totalScale *= newScale
}
}
// Function called when rotate gesture is recognized
@objc private func rotatePiece(_ gestureRecognizer : UIRotationGestureRecognizer) {
guard let piece = gestureRecognizer.view?.subviews.first else { return }
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
piece.transform = piece.transform.rotated(by: gestureRecognizer.rotation)
gestureRecognizer.rotation = 0
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func clamp(value: CGFloat, min: CGFloat, max: CGFloat) -> CGFloat {
if value < min {
return min
} else if value > max {
return max
} else {
return value
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Get UIView from SwiftUI View:
// Get UIView from SwiftUI View
extension View {
var uiView: UIView {
UIHostingController(rootView: self).view
}
}
ContentView:
struct ContentView: View {
var body: some View {
CanvasView()
.frame(width: 880, height: 608)
.transformable()
.background(Color.blue)
}
}
When I plug in my iPad, it appears for a few seconds and prompts me to trust the device, which I do, but a few seconds later it just disappears. When I disconnect and reconnect again, the same thing happens again. I tried restarting my devices, the terminal command, and trying different USB ports, but to no avail. My iPad does show up in Finder however. How can I resolve this issue?
I'm on macOS 13.4, with Xcode 15 beta 2, and iPadOS 17. Any help would be greatly appreciated!
^ This screen appears when I disconnect and reconnect and it prompts me to click trust and enter my password, but then it just suddenly disappears
^This is what shows up when my iPad is disconnected
My playground app runs perfectly fine when I compile it on Xcode on my Mac to run it on my iPad, but for some reason it won't run at all when I try running it directly on Swift Playgrounds (the file opens and it doesn't give me any errors, but it won't compile). How might I be able to fix this?
My app is a .swiftpm file and I'm using Swift Playgrounds 4.0.2 on iPadOS 15.4.1.
Any help would be greatly appreciated!