Errors after changing NSNumber to Int/Double - swiftui

I have this class to get data from Firestore:
struct Spty: Identifiable{
var id: String
var spty: String
var r: NSNumber
var g: NSNumber
var b: NSNumber
}
class SptyViewModel: NSObject, ObservableObject{
#Published var specialities = [Spty]()
#Published var search = ""
func fetchData(){
let db = Firestore.firestore()
db.collection("specialities").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot else {return }
self.specialities = documents.documents.compactMap { (doc) -> Spty? in
let id = doc.documentID
if let spty = doc.get("spty") as? String,
let r = doc.get("r") as? NSNumber,
let g = doc.get("g") as? NSNumber,
let b = doc.get("b") as? NSNumber{
return Spty(id: id, spty: spty, r: r , g: g , b: b )
}
else{
return nil
}
}
}
}
}
And I Displayed it using ForEach:
ForEach(sptyModel.specialities){ spty in
NavigationLink(destination: More()) {
HStack{
Text(spty.spty).foregroundColor(.yellow)
.frame(width: UIScreen.main.bounds.width - 30)
.frame(height: 105)
.background(Color(UIColor(red: CGFloat(truncating: spty.r) / 255.0, green: CGFloat(truncating: spty.g) / 255.0, blue: CGFloat(truncating: spty.b) / 255.0, alpha: 1.0)))
.cornerRadius(38)
.padding(.bottom, 20)
}
}
}
Because of the necessity of the usage of Codable, I need to change NSNumber to Int/Double. After simply just changing it in the class SptyViewModel, I got the following errors in ForEach
How can I solve them

Your problem is not with the ForEach , but the .background(...)
CGFloat(truncating:NSNumber) accept NSNumber only .
ForEach(sptyModel.specialities){ spty in
NavigationLink(destination: Text("MORE")) {
HStack{
Text(spty.spty).foregroundColor(.yellow)
.frame(width: UIScreen.main.bounds.width - 30)
.frame(height: 105)
.background(Color(UIColor(red: CGFloat(truncating: NSNumber(value: spty.r)) / 255.0, green: CGFloat(truncating: NSNumber(value: spty.g)) / 255.0, blue: CGFloat(truncating: NSNumber(value: spty.b)) / 255.0, alpha: 1.0)))
.cornerRadius(38)
.padding(.bottom, 20)
}
}
}

Firebase stores a NSNumber, but you can simply cast it as a Double or Int. Instead of casting "as? NSNumber", just change NSNumber to Int or Double. You'll need to update Spty struct variable types as well.

Related

Read UIColor value from a function

I have a func to retrieve the RGB of an image. In the playground, the function return value as an array [r 0.0 g 0.459 b 0.761 a 1.0]
import Foundation
func findColors(_ image: UIImage) -> [UIColor] {
let pixelsWide = Int(image.size.width)
let pixelsHigh = Int(image.size.height)
guard let pixelData = image.cgImage?.dataProvider?.data else { return [] }
let data: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)
var imageColors: [UIColor] = []
for x in 0..<1 {
for y in 0..<1 {
let point = CGPoint(x: x, y: y)
let pixelInfo: Int = ((pixelsWide * Int(point.y)) + Int(point.x)) * 4
let color = UIColor(red: CGFloat(data[pixelInfo]) / 255.0,
green: CGFloat(data[pixelInfo + 1]) / 255.0,
blue: CGFloat(data[pixelInfo + 2]) / 255.0,
alpha: CGFloat(data[pixelInfo + 3]) / 255.0)
imageColors.append(color)
}
}
return imageColors
}
I tried to display the color based on this RGB array in the ContentView but it gives me an error. Could you please advise?
import SwiftUI
let image = UIImage(named: "Fish.jpg")!
struct ContentView: View {
var body: some View {
Color(findColors(image))
.frame(width:10, height: 10)
.position(x: 40, y: 40)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
i tried to called the function in ContentView but it gives error

How to animate a view in a circular motion using its real-time position coordinates?

I'm currently working on a SwiftUI project, and in order to detect intersections/collisions, I need real-time coordinates, which SwiftUI animations cannot offer. After doing some research, I came across a wonderful question by Kike regarding how to get the real-time coordinates of a view when it is moving/transitioning. And Pylyp Dukhov's answer to that topic recommended utilizing CADisplayLink to calculate the position for each frame and provided a workable solution that did return the real time values when transitioning.
But I'm so unfamiliar with CADisplayLink and creating custom animations that I'm not sure I'll be able to bend it to function the way I want it to.
So this is the animation I want to achieve using CADisplayLink that animates the orange circle view in a circular motion using its position coordinates and repeats forever:
Here is the SwiftUI code:
struct CircleView: View {
#Binding var moveClockwise: Bool
#Binding var duration: Double // Works as speed, since it repeats forever
let geo: GeometryProxy
var body: some View {
ZStack {
Circle()
.stroke()
.frame(width: geo.size.width, height: geo.size.width, alignment: .center)
//MARK: - What I have with SwiftUI animation
Circle()
.fill(.orange)
.frame(width: 35, height: 35, alignment: .center)
.offset(x: -CGFloat(geo.size.width / 2))
.rotationEffect(.degrees(moveClockwise ? 360 : 0))
.animation(
.linear(duration: duration)
.repeatForever(autoreverses: false), value: moveClockwise
)
//MARK: - What I need with CADisplayLink
// Circle()
// .fill(.orange)
// .frame(width: 35, height: 35, alignment: .center)
// .position(CGPoint(x: pos.realTimeX, y: realTimeY))
Button("Start Clockwise") {
moveClockwise = true
// pos.startMovement
}.foregroundColor(.orange)
}.fixedSize()
}
}
struct ContentView: View {
#State private var moveClockwise = false
#State private var duration = 2.0 // Works as speed, since it repeats forever
var body: some View {
VStack {
GeometryReader { geo in
CircleView(moveClockwise: $moveClockwise, duration: $duration, geo: geo)
}
}.padding(20)
}
}
This is what I have currently with CADisplayLink, I added the coordinates to make a circle and that’s about it & it doesn’t repeat forever like the gif does:
Here is the CADisplayLink + real-time coordinate version that I’ve tackled and got lost:
struct Point: View {
var body: some View {
Circle()
.fill(.orange)
.frame(width: 35, height: 35, alignment: .center)
}
}
struct ContentView: View {
#StateObject var P: Position = Position()
var body: some View {
VStack {
ZStack {
Circle()
.stroke()
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width, alignment: .center)
Point()
.position(x: P.realtimePosition.x, y: P.realtimePosition.y)
}
Text("X: \(P.realtimePosition.x), Y: \(P.realtimePosition.y)")
}.onAppear() {
P.startMovement()
}
}
}
class Position: ObservableObject, Equatable {
struct AnimationInfo {
let startDate: Date
let duration: TimeInterval
let startPoint: CGPoint
let endPoint: CGPoint
func point(at date: Date) -> (point: CGPoint, finished: Bool) {
let progress = CGFloat(max(0, min(1, date.timeIntervalSince(startDate) / duration)))
return (
point: CGPoint(
x: startPoint.x + (endPoint.x - startPoint.x) * progress,
y: startPoint.y + (endPoint.y - startPoint.y) * progress
),
finished: progress == 1
)
}
}
#Published var realtimePosition = CGPoint.zero
private var mainTimer: Timer = Timer()
private var executedTimes: Int = 0
private lazy var displayLink: CADisplayLink = {
let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkAction))
displayLink.add(to: .main, forMode: .default)
return displayLink
}()
private let animationDuration: TimeInterval = 0.1
private var animationInfo: AnimationInfo?
private var coordinatesPoints: [CGPoint] {
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
// great progress haha
let radius: Double = Double(screenWidth / 2)
let center = CGPoint(x: screenWidth / 2, y: screenHeight / 2)
var coordinates: [CGPoint] = []
for i in stride(from: 1, to: 360, by: 10) {
let radians = Double(i) * Double.pi / 180 // raiments = degrees * pI / 180
let x = Double(center.x) + radius * cos(radians)
let y = Double(center.y) + radius * sin(radians)
coordinates.append(CGPoint(x: x, y: y))
}
return coordinates
}
// Conform to Equatable protocol
static func ==(lhs: Position, rhs: Position) -> Bool {
// not sure why would you need Equatable for an observable object?
// this is not how it determines changes to update the view
if lhs.realtimePosition == rhs.realtimePosition {
return true
}
return false
}
func startMovement() {
mainTimer = Timer.scheduledTimer(
timeInterval: 0.1,
target: self,
selector: #selector(movePoint),
userInfo: nil,
repeats: true
)
}
#objc func movePoint() {
if (executedTimes == coordinatesPoints.count) {
mainTimer.invalidate()
return
}
animationInfo = AnimationInfo(
startDate: Date(),
duration: animationDuration,
startPoint: realtimePosition,
endPoint: coordinatesPoints[executedTimes]
)
displayLink.isPaused = false
executedTimes += 1
}
#objc func displayLinkAction() {
guard
let (point, finished) = animationInfo?.point(at: Date())
else {
displayLink.isPaused = true
return
}
realtimePosition = point
if finished {
displayLink.isPaused = true
animationInfo = nil
}
}
}
Inside Position you're calculating position related to whole screen. But .position modifier requires value related to the parent view size.
You need to make your calculations based on the parent size, you can use such sizeReader for this purpose:
extension View {
func sizeReader(_ block: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometry in
Color.clear
.onAppear {
block(geometry.size)
}
.onChange(of: geometry.size, perform: block)
}
)
}
}
Usage:
ZStack {
Circle()
.stroke()
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width)
Point()
.position(x: P.realtimePosition.x, y: P.realtimePosition.y)
}
.sizeReader { size in
P.containerSize = size
}
Also CADisplayLink is not used in the right way. The whole point of this tool is that it's already called on each frame, so you can calculate real time position, so your animation is gonna be really smooth, and you don't need a timer or pre-calculated values for only 180(or any other number) positions.
In the linked answer timer was used because a delay was needed between animations, but in your case the code can be greatly simplified:
class Position: ObservableObject {
#Published var realtimePosition = CGPoint.zero
var containerSize: CGSize?
private lazy var displayLink: CADisplayLink = {
let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkAction))
displayLink.add(to: .main, forMode: .default)
displayLink.isPaused = true
return displayLink
}()
private var startDate: Date?
func startMovement() {
startDate = Date()
displayLink.isPaused = false
}
let animationDuration: TimeInterval = 5
#objc func displayLinkAction() {
guard
let containerSize = containerSize,
let timePassed = startDate?.timeIntervalSinceNow,
case let progress = -timePassed / animationDuration,
progress <= 1
else {
displayLink.isPaused = true
startDate = nil
return
}
let frame = CGRect(origin: .zero, size: containerSize)
let radius = frame.midX
let radians = CGFloat(progress) * 2 * .pi
realtimePosition = CGPoint(
x: frame.midX + radius * cos(radians),
y: frame.midY + radius * sin(radians)
)
}
}
I've tried to make more simplified the implementation, here is the SwiftUI code,
struct RotatingDotAnimation: View {
#State private var moveClockwise = false
#State private var duration = 1.0 // Works as speed, since it repeats forever
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 4)
.foregroundColor(.white.opacity(0.5))
.frame(width: 150, height: 150, alignment: .center)
Circle()
.fill(.white)
.frame(width: 18, height: 18, alignment: .center)
.offset(x: -63)
.rotationEffect(.degrees(moveClockwise ? 360 : 0))
.animation(.easeInOut(duration: duration).repeatForever(autoreverses: false),
value: moveClockwise
)
}
.onAppear {
self.moveClockwise.toggle()
}
}
}
It'll basically create animation like this,
enter image description here

"CALayer position contains NaN: [nan nan]" error message caused by custom slider

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()
}
}

How to move scrollview with buttons only in SwiftUI

Previously I did with Swift4 UIScrollView which scrolled with buttons and x offset.
In Swift4 I have:
Set Scrolling Enabled and Paging Enabled to false.
Created the margins, offsets for each frame in UIScrollView and changed the position with buttons Back and Next.
Here is the code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var buttonSound: UIButton!
#IBOutlet weak var buttonPrev: UIButton!
#IBOutlet weak var buttonNext: UIButton!
#IBOutlet weak var scrollView: UIScrollView!
var levels = ["level1", "level2", "level3", "level4"]
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
var currentLevel = 1
var previousLevel: Int? = nil
override func viewDidLoad() {
super.viewDidLoad()
//Defining the Various Swipe directions (left, right, up, down)
let swipeLeft = UISwipeGestureRecognizer(target: self, action: #selector(self.handleGesture(gesture:)))
swipeLeft.direction = .left
self.view.addGestureRecognizer(swipeLeft)
let swipeRight = UISwipeGestureRecognizer(target: self, action: #selector(self.handleGesture(gesture:)))
swipeRight.direction = .right
self.view.addGestureRecognizer(swipeRight)
addHorizontalLevelsList()
customizeButtons()
resizeSelected()
}
func addHorizontalLevelsList() {
var frame : CGRect?
for i in 0..<levels.count {
let button = UIButton(type: .custom)
let buttonW = screenWidth/3
let buttonH = screenHeight/2
frame = CGRect(x: CGFloat(i+1) * (screenWidth/2) - (buttonW/2),
y: buttonH - 100,
width: buttonW,
height: buttonH)
button.frame = frame!
button.tag = i+1
button.backgroundColor = .lightGray
button.addTarget(self, action: #selector(selectTeam), for: .touchUpInside)
button.setTitle(levels[i], for: .normal)
scrollView.addSubview(button)
}
scrollView.contentSize = CGSize(width: (screenWidth/2 * CGFloat(levels.count)),
height: screenHeight)
scrollView.backgroundColor = .clear
self.view.addSubview(scrollView)
}
func customizeButtons(){
buttonPrev.frame = CGRect(x: 0,
y: (screenHeight/2) - 40,
width: 80, height: 80)
buttonNext.frame = CGRect(x: screenWidth - 80,
y: (screenHeight/2) - 40,
width: 80, height: 80)
buttonPrev.superview?.bringSubviewToFront(buttonPrev)
buttonNext.superview?.bringSubviewToFront(buttonNext)
}
#objc func selectTeam(button: UIButton) {
button.transform = CGAffineTransform(scaleX: 0.6, y: 0.6)
UIView.animate(withDuration: 1.0,
delay: 0,
usingSpringWithDamping: CGFloat(0.20),
initialSpringVelocity: CGFloat(6.0),
options: UIView.AnimationOptions.allowUserInteraction,
animations: {
button.transform = CGAffineTransform.identity
},
completion: { Void in() }
)
print(levels[button.tag])
let vc = PopTypeVC(nibName: "PopTypeVC", bundle: nil)
vc.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
self.present(vc, animated: true)
}
#IBAction func prevLevel(_ sender: Any) {
if currentLevel > 0 {
currentLevel -= 1
scroll()
}
}
#IBAction func nextLevel(_ sender: Any) {
if currentLevel < levels.count {
currentLevel += 1
scroll()
}
}
func scroll(){
print(currentLevel)
print(previousLevel as Any)
scrollView.setContentOffset(CGPoint(x: currentLevel * Int(screenWidth/2), y: 0), animated: true)
resizeSelected()
}
// The #objc before func is a must, since we are using #selector (above)
#objc func handleGesture(gesture: UISwipeGestureRecognizer) -> Void {
if gesture.direction == UISwipeGestureRecognizer.Direction.right {
prevLevel(self)
}
else if gesture.direction == UISwipeGestureRecognizer.Direction.left {
nextLevel(self)
}
}
func resizeSelected(){
if previousLevel != nil {
let previousFrame = CGRect(x:CGFloat(previousLevel!) * (screenWidth/2) - (screenWidth/3)/2,
y: (screenHeight/2) - 100,
width: screenWidth/3,
height: screenHeight/2)
scrollView.viewWithTag(previousLevel!)?.frame = previousFrame
}
let currentFrame = CGRect(x: CGFloat(currentLevel) * (screenWidth/2) - (screenWidth/3)/2 - 10,
y: (screenHeight/2) - 110,
width: screenWidth/3 + 20,
height: screenHeight/2 + 20)
scrollView.viewWithTag(currentLevel)?.frame = currentFrame
previousLevel = currentLevel
}
}
The problem is I can't do this with SwiftUI:
struct ContentView: View {
static var levels = ["level1",
"level2",
"level3",
"level4"]
var currentLevel = 1
var previousLevel: Int? = nil
let screenW = UIScreen.main.bounds.width
let screenH = UIScreen.main.bounds.height
let margin1 = 50
let margin2 = 100
let margin3 = 20
let sceneButtonW = 100
let buttonPadding = 40
var body: some View {
ZStack {
// Horizontal list
VStack {
Spacer()
.frame(height: margin2)
ScrollView(.horizontal, showsIndicators: false) {
HStack{
Spacer()
.frame(width: buttonPadding + sceneButtonW/2)
ForEach(0..<ContentView.levels.count) { i in
cardView(i: i).tag(i+1)
}
Spacer()
.frame(width: buttonPadding + sceneButtonW/2)
}
}
Spacer()
.frame(height: margin3)
}
}
.background(Image("bg")
.resizable()
.edgesIgnoringSafeArea(.all)
.aspectRatio(contentMode: .fill))
}
}
Question: Are there any methods to disable automatic scrolling at all and use offsets at ScrollView with SwiftUI?
This already built solution for SwiftUI
https://github.com/fermoya/SwiftUIPager
However, there is no real example.

Image "carousel" animation in SwiftUI

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
})