I'm recreating an Objective-C app in SwiftUI. In my old app, I have a full-screen background image that has a slide transition between several images. Here's the Objective-C code:
- (void)viewWillAppear:(BOOL)animated{
self.output.text = [self.output.text stringByAppendingString:#"viewWillAppear\r\n"];
[self resetCardReader];
self.creditCard = [[FWCCreditCard alloc] init];
_imageArray = [[NSMutableArray alloc] initWithObjects:[UIImage imageNamed:#"Family on Beach.png"],
[UIImage imageNamed:#"Chinese Graduate.png"],
[UIImage imageNamed:#"New Car.png"],
[UIImage imageNamed:#"House Sold.png"], nil];
index = 0;
}
- (void)viewDidAppear:(BOOL)animated{
self.slideTransition = [CATransition animation]; // CATransition * slideTransition; instance variable
self.slideTransition.duration = 2.0;
self.slideTransition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
self.slideTransition.type = kCATransitionPush;
self.slideTransition.delegate = self;
self.slideTransition.subtype =kCATransitionFromRight; // or kCATransitionFromLeft
self.repeatingTimer = [NSTimer scheduledTimerWithTimeInterval:10.0 target:self selector:#selector(slideShow) userInfo:nil repeats:YES];
[self.repeatingTimer fire];
}
-(void)slideShow
{
[self.BackgroundImage.layer addAnimation:self.slideTransition forKey:nil];
if (index < self.imageArray.count-1) // NSUInteger index; instance variable
{
index++;
}
else
{
index=0;
}
self.BackgroundImage.image =[self.imageArray objectAtIndex:index];
}
Here's what I have so far in SwiftUI. Can anyone give me some pointers on where to add the animation?
struct HomeView : View {
private var backgroundImages = ["Family on Beach","Chinese Graduate","New Car","House Sold"]
private var backgroundImageIndex = 0
#State private var backgroundImage = "Family on Beach"
var body: some View {
ZStack{
Image(backgroundImage)
.resizable()
.aspectRatio(contentMode: .fill)
.colorMultiply(Color(red: 0.09, green: 0.286, blue: 0.486, opacity: 0.8))
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: true)
OK, I got it:
import SwiftUI
extension AnyTransition {
static var rightToLeft: AnyTransition {
let insertion = AnyTransition.move(edge: .trailing)
let removal = AnyTransition.move(edge: .leading)
return .asymmetric(insertion: insertion, removal: removal)
}
}
struct HomeView : View {
private var backgroundImages = ["Family on Beach","Chinese Graduate","New Car","House Sold"]
#State private var backgroundImageIndex = 0
#State private var backgroundImageNameEven = "Family on Beach"
#State private var backgroundImageNameOdd = "Chinese Graduate"
#State private var showImage = true
var timer: Timer {
Timer.scheduledTimer(withTimeInterval: 10, repeats: true){_ in
if(self.backgroundImageIndex < self.backgroundImages.count-1){
self.backgroundImageIndex += 1
} else
{
self.backgroundImageIndex = 0
}
if(self.backgroundImageIndex % 2 == 0){
self.backgroundImageNameEven = self.backgroundImages[self.backgroundImageIndex]
} else {
self.backgroundImageNameOdd = self.backgroundImages[self.backgroundImageIndex]
}
withAnimation{
self.showImage.toggle()
}
}
}
var OddImage: some View {
Image(backgroundImageNameOdd)
.resizable()
.clipped()
.colorMultiply(Color(red: 0.09, green: 0.286, blue: 0.486, opacity: 0.8))
.edgesIgnoringSafeArea(.top)
.aspectRatio(contentMode: .fill)
.animation(.basic(duration: 2))
.transition(.rightToLeft)
}
var EvenImage: some View {
Image(backgroundImageNameEven)
.resizable()
.clipped()
.colorMultiply(Color(red: 0.09, green: 0.286, blue: 0.486, opacity: 0.8))
.edgesIgnoringSafeArea(.top)
.aspectRatio(contentMode: .fill)
.animation(.basic(duration: 2))
.transition(.rightToLeft)
}
var body: some View {
ZStack{
if(!self.showImage){
OddImage
}
if(self.showImage){
EvenImage
}
Just add the following to any other element on the view to trigger the timer on view:
.onAppear(perform: {
let _ = self.timer
})
Related
What I'm trying to do is basically having some images in a TabView and being able to zoom and pan on those images.
Right now I got this quite nicely implemented but I struggle to find a solution so that it is not possible to pan outside the bounds of the image.
This is what I currently have:
I want to try having the image clip to the side of the screen so you don't pan the image out of the visible area.
So far this it what my code looks like:
struct FinalImageSwipeView: View {
#ObservedObject var viewModel: ImageChatViewModel
#State var currentImage: UUID
#State var fullPreview: Bool = false
#GestureState var draggingOffset: CGSize = .zero
var body: some View {
TabView(selection: $currentImage) {
ForEach(viewModel.images) { image in
GeometryReader{ proxy in
let size = proxy.size
PinchAndPanImage(image: UIImage(named: image.imageName)!,
fullPreview: $fullPreview)
.frame(width: size.width, height: size.height)
.contentShape(Rectangle())
}
.tag(image.id)
.ignoresSafeArea()
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
.ignoresSafeArea()
// upper navigation bar
.overlay(
ImageSwipeViewNavigationBar(fullPreview: $fullPreview, hideSwipeView: viewModel.hideSwipeView),
alignment: .top
)
// bottom image scrollview
.overlay(
ImageSwipeViewImageSelection(viewModel: viewModel,
currentImage: $currentImage,
fullPreview: $fullPreview),
alignment: .bottom
)
.gesture(DragGesture().updating($draggingOffset, body: { (value, outValue, _) in
if viewModel.imageScale == 0 {
outValue = value.translation
viewModel.onChangeDragGesture(value: draggingOffset)
}}).onEnded({ (value) in
if viewModel.imageScale == 0 {
viewModel.onEnd(value: value)
}
}))
.transition(.offset(y: UIScreen.main.bounds.size.height + 100))
}
}
struct ImageSwipeViewImageSelection: View {
#ObservedObject var viewModel: ImageChatViewModel
#Binding var currentImage: UUID
#Binding var fullPreview: Bool
var body: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: true) {
HStack(spacing: 15) {
ForEach(viewModel.images) { image in
Image(image.imageName)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 70, height: 60)
.cornerRadius(12)
.id(image.id)
.overlay {
RoundedRectangle(cornerRadius: 12)
.stroke(image.id == currentImage ? Color.white : Color.clear, lineWidth: 2)
}
.onTapGesture {
currentImage = image.id
}
}
}
.padding()
}
.frame(height: 80)
.background(BlurView(style: .systemUltraThinMaterialDark).ignoresSafeArea(edges: .bottom))
// While current post changing center current image in scrollview
.onAppear(perform: {
proxy.scrollTo(currentImage, anchor: .bottom)
})
.onChange(of: currentImage) { _ in
viewModel.imageScale = 1
withAnimation {
proxy.scrollTo(currentImage, anchor: .bottom)
}
}
}
.offset(y: fullPreview ? 150 : 0)
}
}
struct PinchAndPanImage: View {
let image: UIImage
#Binding var fullPreview: Bool
// Stuff for Pinch and Pan
#State var imageScale: CGFloat = 1
#State var imageCurrentScale: CGFloat = 0
#State var imagePanOffset: CGSize = .zero
#State var currentImagePanOffset: CGSize = .zero
var usedImageScale: CGFloat {
max(1, min(imageScale + imageCurrentScale, 10))
}
var usedImagePan: CGSize {
let width = imagePanOffset.width + currentImagePanOffset.width
let height = imagePanOffset.height + currentImagePanOffset.height
return CGSize(width: width, height: height)
}
var body: some View {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(0)
.offset(usedImagePan)
.scaleEffect(usedImageScale > 1 ? usedImageScale : 1)
.gesture(
// Magnifying Gesture
MagnificationGesture()
.onChanged({ value in
imageCurrentScale = value - 1
})
.onEnded({ value in
imageCurrentScale = 0
imageScale = imageScale + value - 1
withAnimation(.easeInOut) {
if imageScale > 5 {
imageScale = 5
}
}
})
)
.simultaneousGesture(createPanGesture())
.onTapGesture(count: 2) {
withAnimation {
imageScale = 1
imagePanOffset = .zero
}
}
.onTapGesture(count: 1) {
withAnimation {
fullPreview.toggle()
}
}
}
private func createPanGesture() -> _EndedGesture<_ChangedGesture<DragGesture>>? {
let gesture = DragGesture()
.onChanged { value in
let width = value.translation.width / usedImageScale
let height = value.translation.height / usedImageScale
currentImagePanOffset = CGSize(width: width, height: height)
}
.onEnded { value in
currentImagePanOffset = .zero
let scaledWidth = value.translation.width / usedImageScale
let scaledHeight = value.translation.height / usedImageScale
let width = imagePanOffset.width + scaledWidth
let height = imagePanOffset.height + scaledHeight
imagePanOffset = CGSize(width: width, height: height)
}
return imageScale > 1 ? gesture : nil
}
}
I created an Image with Drag gesture, but when I convert it to a Button, full screen become the button, so when I click to anywhere in the screen, the Button will be action
struct OwlFly: View {
private var bround = UIScreen.main.bounds
#State var isShow = false
#State private var location = CGPoint(x: 60, y: 60)
#GestureState private var startLocation: CGPoint? = nil
var simpleDrag: some Gesture {
DragGesture()
.onChanged { value in
var newLocation = startLocation ?? location
newLocation.x += value.translation.width
newLocation.y += value.translation.height
self.location = newLocation
self.location = value.location
}
.onEnded{ value in
if(value.translation.width > bround.size.width/2) {
self.location.x = bround.size.width - 30
}
else {
self.location.x = 30
}
}
.updating($startLocation) { (value, startLocation, transaction) in
startLocation = startLocation ?? location
}
}
var body: some View {
Button(action: {
self.isShow.toggle()
if(isFly) {
self.location.x = bround.width/2
}
else {
self.location.x = 30
}
}) { // I convert the Image to a label of button
Image("simpleDrag")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50)
.animation(.easeInOut)
.position(location)
.gesture(simpleDrag)
}
}
}
That is my code
This is nice!
The .position expands the view (and the button) to maximum size, that's why you can click everywhere.
The easiest workaround is not using a Button, but making the image itself tappable – see code below.
PS: You don't have to use #GestureState if you manage the dragging by yourself with onChanged and onEnded – you did a double job. I commented out everything you don't need ;)
struct ContentView: View {
private var bround = UIScreen.main.bounds
#State var isShow = false
#State private var location = CGPoint(x: 60, y: 60)
// #GestureState private var startLocation: CGPoint? = nil
var simpleDrag: some Gesture {
DragGesture()
.onChanged { value in
// var newLocation = location
// newLocation.x += value.translation.width
// newLocation.y += value.translation.height
// self.location = newLocation
self.location = value.location
}
.onEnded{ value in
if(value.translation.width > bround.size.width/2) {
self.location.x = bround.size.width - 30
}
else {
self.location.x = 30
}
}
// .updating($startLocation) { (value, startLocation, transaction) in
// startLocation = startLocation ?? location
// }
}
var body: some View {
Image(systemName: "bubble.right.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50)
.animation(.easeInOut, value: location)
.position(location)
.gesture(simpleDrag)
.onTapGesture {
self.isShow.toggle()
if(isShow) {
self.location.x = bround.width/2
}
else {
self.location.x = 30
}
}
}
}
Tried several way to make it bubble effect from bottom to top moving.
I able to move it from bottom to top. but I can not make it like bubble effect.
#State private var bouncing = true
var body: some View {
Image("bubble").resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 40)
.frame(maxHeight: .infinity, alignment: bouncing ? .bottom : .top)
.animation(Animation.easeInOut(duration: 5.0).repeatForever(autoreverses: false))
.onAppear {
self.bouncing.toggle()
}
}
Here is simple bubble animation what I looking for.
import SwiftUI
struct MyParentView: View {
#State var replay: Bool = false
var body: some View {
ZStack{
Color.blue.opacity(0.8)
BubbleEffectView(replay: $replay)
VStack{
Spacer()
Button(action: {
replay.toggle()
}, label: {Text("replay")}).foregroundColor(.red)
}
}
}
}
struct BubbleEffectView: View {
#StateObject var viewModel: BubbleEffectViewModel = BubbleEffectViewModel()
#Binding var replay: Bool
var body: some View {
GeometryReader{ geo in
ZStack{
//Show bubble views for each bubble
ForEach(viewModel.bubbles){bubble in
BubbleView(bubble: bubble)
}
}.onChange(of: replay, perform: { _ in
viewModel.addBubbles(frameSize: geo.size)
})
.onAppear(){
//Set the initial position from frame size
viewModel.viewBottom = geo.size.height
viewModel.addBubbles(frameSize: geo.size)
}
}
}
}
class BubbleEffectViewModel: ObservableObject{
#Published var viewBottom: CGFloat = CGFloat.zero
#Published var bubbles: [BubbleViewModel] = []
private var timer: Timer?
private var timerCount: Int = 0
#Published var bubbleCount: Int = 50
func addBubbles(frameSize: CGSize){
let lifetime: TimeInterval = 2
//Start timer
timerCount = 0
if timer != nil{
timer?.invalidate()
}
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { (timer) in
let bubble = BubbleViewModel(height: 10, width: 10, x: frameSize.width/2, y: self.viewBottom, color: .white, lifetime: lifetime)
//Add to array
self.bubbles.append(bubble)
//Get rid if the bubble at the end of its lifetime
Timer.scheduledTimer(withTimeInterval: bubble.lifetime, repeats: false, block: {_ in
self.bubbles.removeAll(where: {
$0.id == bubble.id
})
})
if self.timerCount >= self.bubbleCount {
//Stop when the bubbles will get cut off by screen
timer.invalidate()
self.timer = nil
}else{
self.timerCount += 1
}
}
}
}
struct BubbleView: View {
//If you want to change the bubble's variables you need to observe it
#ObservedObject var bubble: BubbleViewModel
#State var opacity: Double = 0
var body: some View {
Circle()
.foregroundColor(bubble.color)
.opacity(opacity)
.frame(width: bubble.width, height: bubble.height)
.position(x: bubble.x, y: bubble.y)
.onAppear {
withAnimation(.linear(duration: bubble.lifetime)){
//Go up
self.bubble.y = -bubble.height
//Go sideways
self.bubble.x += bubble.xFinalValue()
//Change size
let width = bubble.yFinalValue()
self.bubble.width = width
self.bubble.height = width
}
//Change the opacity faded to full to faded
//It is separate because it is half the duration
DispatchQueue.main.asyncAfter(deadline: .now()) {
withAnimation(.linear(duration: bubble.lifetime/2).repeatForever()) {
self.opacity = 1
}
}
DispatchQueue.main.asyncAfter(deadline: .now()) {
withAnimation(Animation.linear(duration: bubble.lifetime/4).repeatForever()) {
//Go sideways
//bubble.x += bubble.xFinalValue()
}
}
}
}
}
class BubbleViewModel: Identifiable, ObservableObject{
let id: UUID = UUID()
#Published var x: CGFloat
#Published var y: CGFloat
#Published var color: Color
#Published var width: CGFloat
#Published var height: CGFloat
#Published var lifetime: TimeInterval = 0
init(height: CGFloat, width: CGFloat, x: CGFloat, y: CGFloat, color: Color, lifetime: TimeInterval){
self.height = height
self.width = width
self.color = color
self.x = x
self.y = y
self.lifetime = lifetime
}
func xFinalValue() -> CGFloat {
return CGFloat.random(in:-width*CGFloat(lifetime*2.5)...width*CGFloat(lifetime*2.5))
}
func yFinalValue() -> CGFloat {
return CGFloat.random(in:0...width*CGFloat(lifetime*2.5))
}
}
struct MyParentView_Previews: PreviewProvider {
static var previews: some View {
MyParentView()
}
}
I have a video player that starts playing a video (from firebase URL) and in some cases (70% of cases) i get this error message (exception) when running on physical device (no issues when launching in simulator though):
"CALayer position contains NaN: [nan nan]"
I found that the error doesn't appear when i comment "VideoPlayerControlsView()", so i'm pretty sure the problem is is my CustomerSlider object located insider of this VideoPlayerControlsView view.
I think it may somehow be caused by loading remote video, as the video is not loaded, the app doesn't know the size/bounds of AVPlayer object and therefore some parent view (maybe CustomerSlider) can't be created..
Building a Minimal Reproducible Example would be a nightmare, i just hope some can find a mistake in my code/logic.. If not - gonna build it of course. No other choice.
struct DetailedPlayerView : View {
// The progress through the video, as a percentage (from 0 to 1)
#State private var videoPos: Double = 0
// The duration of the video in seconds
#State private var videoDuration: Double = 0
// Whether we're currently interacting with the seek bar or doing a seek
#State private var seeking = false
private var player: AVPlayer = AVPlayer()
init(item: ExerciseItem, hVideoURL: URL?) {
if hVideoURL != nil {
player = AVPlayer(url: hVideoURL!)
player.isMuted = true
player.play()
} else {
print("[debug] hVideoURL is nil")
}
}
var body: some View {
ZStack {
//VStack {
VideoPlayerView(videoPos: $videoPos,
videoDuration: $videoDuration,
seeking: $seeking,
//timeline: $timeline,
//videoTimeline: videoTimeline,
player: player)
.frame(width: UIScreen.screenHeight, height: UIScreen.screenWidth)
VStack {
Spacer()
VideoPlayerControlsView(videoPos: $videoPos, **<<-----------------------**
videoDuration: $videoDuration,
seeking: $seeking,
player: player)
.frame(width: UIScreen.screenHeight - 2*Constants.scrollPadding, height: 20)
.padding(.bottom, 20)
}
}
.onDisappear {
// When this View isn't being shown anymore stop the player
self.player.replaceCurrentItem(with: nil)
}
}
}
struct VideoPlayerControlsView : View {
#Binding private(set) var videoPos: Double
#Binding private(set) var videoDuration: Double
#Binding private(set) var seeking: Bool
// #Binding private(set) var timeline: [Advice]
#State var shouldStopPlayer: Bool = false
#State var player: AVPlayer
//let player: AVPlayer
#State private var playerPaused = false
var body: some View {
HStack {
// Play/pause button
Button(action: togglePlayPause) {
Image(systemName: playerPaused ? "arrowtriangle.right.fill" : "pause.fill")
.foregroundColor(Color.mainSubtitleColor)
.contentShape(Rectangle())
.padding(.trailing, 10)
}
// Current video time
if videoPos.isFinite && videoPos.isCanonical && videoDuration.isFinite && videoDuration.isCanonical {
Text(Utility.formatSecondsToHMS(videoPos * videoDuration))
.foregroundColor(Color.mainSubtitleColor)
}
// Slider for seeking / showing video progress
CustomSlider(value: $videoPos, shouldStopPlayer: self.$shouldStopPlayer, range: (0, 1), knobWidth: 4) { modifiers in
ZStack {
Group {
Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 0.5799999833106995))//Color((red: 0.4, green: 0.3, blue: 1)
.opacity(0.4)
.frame(height: 4)
.modifier(modifiers.barRight)
Color.mainSubtitleColor//Color(red: 0.4, green: 0.3, blue: 1)
.frame(height: 4)
.modifier(modifiers.barLeft)
}
.cornerRadius(5)
VStack {
Image(systemName: "arrowtriangle.down.fill") // SF Symbol
.foregroundColor(Color.mainSubtitleColor)
.offset(y: -3)
}
.frame(width: 20, height: 20)
.contentShape(Rectangle())
.modifier(modifiers.knob)
}
}
.onChange(of: shouldStopPlayer) { _ in
if shouldStopPlayer == false {
print("[debug] shouldStopPlayer == false")
sliderEditingChanged(editingStarted: false)
} else {
if seeking == false {
print("[debug] shouldStopPlayer == true")
sliderEditingChanged(editingStarted: true)
}
}
}
.frame(height: 20)
// Video duration
if videoDuration.isCanonical && videoDuration.isFinite {
Text(Utility.formatSecondsToHMS(videoDuration))
.foregroundColor(Color.mainSubtitleColor)
}
}
.padding(.leading, 40)
.padding(.trailing, 40)
}
private func togglePlayPause() {
pausePlayer(!playerPaused)
}
private func pausePlayer(_ pause: Bool) {
playerPaused = pause
if playerPaused {
player.pause()
}
else {
player.play()
}
}
private func sliderEditingChanged(editingStarted: Bool) {
if editingStarted {
// Set a flag stating that we're seeking so the slider doesn't
// get updated by the periodic time observer on the player
seeking = true
pausePlayer(true)
}
// Do the seek if we're finished
if !editingStarted {
let targetTime = CMTime(seconds: videoPos * videoDuration,
preferredTimescale: 600)
player.seek(to: targetTime) { _ in
// Now the seek is finished, resume normal operation
self.seeking = false
self.pausePlayer(false)
}
}
}
}
extension Double {
func convert(fromRange: (Double, Double), toRange: (Double, Double)) -> Double {
// Example: if self = 1, fromRange = (0,2), toRange = (10,12) -> solution = 11
var value = self
value -= fromRange.0
value /= Double(fromRange.1 - fromRange.0)
value *= toRange.1 - toRange.0
value += toRange.0
return value
}
}
struct CustomSliderComponents {
let barLeft: CustomSliderModifier
let barRight: CustomSliderModifier
let knob: CustomSliderModifier
}
struct CustomSliderModifier: ViewModifier {
enum Name {
case barLeft
case barRight
case knob
}
let name: Name
let size: CGSize
let offset: CGFloat
func body(content: Content) -> some View {
content
.frame(width: (size.width >= 0) ? size.width : 0)
.position(x: size.width*0.5, y: size.height*0.5)
.offset(x: offset)
}
}
struct CustomSlider<Component: View>: View {
#Binding var value: Double
var range: (Double, Double)
var knobWidth: CGFloat?
let viewBuilder: (CustomSliderComponents) -> Component
#Binding var shouldStopPlayer: Bool
init(value: Binding<Double>, shouldStopPlayer: Binding<Bool>, range: (Double, Double), knobWidth: CGFloat? = nil, _ viewBuilder: #escaping (CustomSliderComponents) -> Component
) {
_value = value
_shouldStopPlayer = shouldStopPlayer
self.range = range
self.viewBuilder = viewBuilder
self.knobWidth = knobWidth
}
var body: some View {
return GeometryReader { geometry in
self.view(geometry: geometry) // function below
}
}
private func view(geometry: GeometryProxy) -> some View {
let frame = geometry.frame(in: .global)
let drag = DragGesture(minimumDistance: 0)
.onChanged { drag in
shouldStopPlayer = true
self.onDragChange(drag, frame)
}
.onEnded { drag in
shouldStopPlayer = false
//self.updatedValue = value
print("[debug] slider drag gesture ended, value = \(value)")
}
let offsetX = self.getOffsetX(frame: frame)
let knobSize = CGSize(width: knobWidth ?? frame.height, height: frame.height)
let barLeftSize = CGSize(width: CGFloat(offsetX + knobSize.width * 0.5), height: frame.height)
let barRightSize = CGSize(width: frame.width - barLeftSize.width, height: frame.height)
let modifiers = CustomSliderComponents(
barLeft: CustomSliderModifier(name: .barLeft, size: barLeftSize, offset: 0),
barRight: CustomSliderModifier(name: .barRight, size: barRightSize, offset: barLeftSize.width),
knob: CustomSliderModifier(name: .knob, size: knobSize, offset: offsetX))
return ZStack { viewBuilder(modifiers).gesture(drag) }
}
private func onDragChange(_ drag: DragGesture.Value,_ frame: CGRect) {
let width = (knob: Double(knobWidth ?? frame.size.height), view: Double(frame.size.width))
let xrange = (min: Double(0), max: Double(width.view - width.knob))
var value = Double(drag.startLocation.x + drag.translation.width) // knob center x
value -= 0.5*width.knob // offset from center to leading edge of knob
value = value > xrange.max ? xrange.max : value // limit to leading edge
value = value < xrange.min ? xrange.min : value // limit to trailing edge
value = value.convert(fromRange: (xrange.min, xrange.max), toRange: range)
//print("[debug] slider drag gesture detected, value = \(value)")
self.value = value
}
private func getOffsetX(frame: CGRect) -> CGFloat {
let width = (knob: knobWidth ?? frame.size.height, view: frame.size.width)
let xrange: (Double, Double) = (0, Double(width.view - width.knob))
let result = self.value.convert(fromRange: range, toRange: xrange)
return CGFloat(result)
}
}
some extra code showing how DetailedPlayerView is triggered:
struct DetailedVideo: View {
var item: ExerciseItem
var url: URL
#Binding var isPaused: Bool
var body: some View {
ZStack {
DetailedPlayerView(item: self.item, hVideoURL: url)
//.frame(width: 500, height: 500) //##UPDATED: Apr 10
HStack {
VStack {
ZStack {
//Rectangle 126
RoundedRectangle(cornerRadius: 1)
.fill(Color(#colorLiteral(red: 0.3063802123069763, green: 0.3063802123069763, blue: 0.3063802123069763, alpha: 1)))
.frame(width: 2, height: 20.3)
.rotationEffect(.degrees(-135))
//Rectangle 125
RoundedRectangle(cornerRadius: 1)
.fill(Color(#colorLiteral(red: 0.3063802123069763, green: 0.3063802123069763, blue: 0.3063802123069763, alpha: 1)))
.frame(width: 2, height: 20.3)
.rotationEffect(.degrees(-45))
}
.frame(width: 35, height: 35)//14.4
.contentShape(Rectangle())
.onTapGesture {
print("[debugUI] isPaused = false")
self.isPaused = false
}
.offset(x:20, y:20)
Spacer()
}
Spacer()
}
}
.ignoresSafeArea(.all)
}
}
#ViewBuilder
var detailedVideoView: some View {
if self.hVideoURL != nil {
DetailedVideo(item: self.exerciseVM.exerciseItems[self.exerciseVM.currentIndex], url: self.hVideoURL!, isPaused: self.$exerciseVM.isPaused) // when is paused - we are playing detailed video?
.frame(width: UIScreen.screenHeight, height: UIScreen.screenWidth) //UPDATED: Apr 9, 2021
.onAppear {
AppDelegate.orientationLock = UIInterfaceOrientationMask.landscapeLeft
UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
}
.onDisappear {
DispatchQueue.main.async {
AppDelegate.orientationLock = UIInterfaceOrientationMask.portrait
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
}
}
} else {
EmptyView()
}
}
I'm trying to make animation when I tap on view it becomes full screen and when I drag down view it scales down and returns to its previous state. I use matchedGeometryEffect with two views and change destination view frame with DragGesture, transition to source view works «unexpectedly». How to fix it or how to make this animation correct? If I don't change destination view frame it works as I expect (click button). GIF here: https://i.stack.imgur.com/yXsjF.gif
struct Test4: View {
#Namespace var animation
#State private var show = false
#State private var scale: CGFloat = 1
var body: some View {
ZStack(alignment: .topTrailing) {
if show {
ScrollView {
Color.gray
.border(Color.black, width: 30)
.matchedGeometryEffect(id: "animation", in: animation)
.frame(
width: UIScreen.main.bounds.width * scale,
height: UIScreen.main.bounds.height * scale)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged(onChanged)
.onEnded(onEnded)
)
}
.ignoresSafeArea()
Button("go back") {
withAnimation(Animation.easeInOut(duration: 1)) {
self.show.toggle()
}
}
} else {
VStack {
Color.gray
.border(Color.black, width: 30)
.matchedGeometryEffect(id: "animation", in: animation)
.frame(width: 200, height: 200)
.onTapGesture {
withAnimation {
self.scale = 1
show.toggle()
}
}
}
}
}
}
func onChanged(value: DragGesture.Value) {
withAnimation(Animation.easeInOut(duration: 3)) {
let currentScale = value.translation.height / UIScreen.main.bounds.height
if currentScale <= 0 {
return
}
let newScale = 1 - currentScale
if newScale > 0.85 {
self.scale = newScale
} else if newScale < 0.85 {
self.show = false
}
}
}
func onEnded(value: DragGesture.Value) {
withAnimation(Animation.easeInOut(duration: 3)) {
if self.scale < 0.85 {
self.show = false
}
}
}
}
struct Test4_Previews: PreviewProvider {
static var previews: some View {
Test4()
}
}
The thing that you are trying get is possible without matchedGeometryEffect, here is a simple approach:
import SwiftUI
struct ContentView: View {
var body: some View {
ScaleView()
}
}
struct ScaleView: View {
#State private var translation: CGFloat = CGFloat()
#State private var lastTranslation: CGFloat = CGFloat()
var body: some View {
GeometryReader { geometry in
Color.black
ZStack {
Color
.gray
Image(systemName: "arrow.triangle.2.circlepath.circle")
.font(Font.largeTitle)
.onTapGesture {
if lastTranslation == 400.0 {
lastTranslation = 0.0
translation = lastTranslation
}
else {
lastTranslation = 400.0
translation = lastTranslation
}
}
}
.position(x: geometry.size.width/2, y: geometry.size.height/2)
.cornerRadius(30)
.scaleEffect(1.0 - translation/(geometry.size.width > geometry.size.height ? geometry.size.width : geometry.size.height))
.gesture(DragGesture(minimumDistance: 0.0).onChanged(onChanged).onEnded(onEnded))
}
.ignoresSafeArea()
.animation(.easeInOut(duration: 0.35))
.statusBar(hidden: true)
}
func onChanged(value: DragGesture.Value) { translation = lastTranslation + value.translation.height }
func onEnded(value: DragGesture.Value) {
if value.translation.height > 0.0 {
lastTranslation = 400.0
translation = lastTranslation
}
else {
lastTranslation = 0.0
translation = lastTranslation
}
}
}