SwiftUI MVVM #Environment Breaks - xcode

I'm trying to do a fairly simple View with MVVM to be a good ViewModel citizen. However,
the code breaks while accessing the #Enviromnent Core Data in the ViewModel. I created
two functions in the ViewModel. One accesses Core Data through the #Environment and one
accesses Core Data with the old style - get a reference to AppDelegate and do my own
thing. The OldSchool method works. Comment 2 below. The #Environment does not - it breaks
at the line indicated below with an error that is not helpful for me. Comment 1.
(Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0))
Now if I take the same #Environment code and put it directly into the view it Works.
And if I call the same line that breaks the MVVM in a Text in the View I get the
correct response. Comment 2
This is the view:
struct UserUtilities: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: DermPhoto.getAllDermPhotos()) var dermPhotos: FetchedResults<DermPhoto>
#State private var reminderInterval = 1
#State private var enableReminders = true
#State private var daysSincePhoto: Int = 0
#ObservedObject var userUtilitiesVM = UserUtilitiesViewModel()
var body: some View {
NavigationView {
VStack {
Group { //group 1
Toggle(isOn: $enableReminders) {
Text("Enable Reminders")
}
.padding(EdgeInsets(top: 50, leading: 50, bottom: 0, trailing: 50))
Text("Reminders are" + (enableReminders == true ? " On" : " Off"))
Spacer()
//Comment 3 - this always works
Text("String interpolation of self.dermPhotos.count")
Text("\(self.dermPhotos.count)")
} //group 1
Group { //group 2
Text("It has been " + "\(self.daysSincePhoto) " + (daysSincePhoto == 1 ? "day" : "days") + " since a photo was added.")
.padding(EdgeInsets(top: 0, leading: 50, bottom: 0, trailing: 50))
//options for the sentence above
//\(self.userUtilitiesVM.getTheDateInterval())
//\(self.userUtilitiesVM.getFromEnvironment())
//\(self.userUtilitiesVM.dateInterval)
Spacer()
Stepper("Reminder Interval", value: $reminderInterval, in: 1 ... 30)
.padding(EdgeInsets(top: 0, leading: 50, bottom: 0, trailing: 50))
Text("Reminder Interval is: \(reminderInterval)" + (reminderInterval == 1 ? " day" : " days"))
Spacer()
}//group 2
}
.navigationBarTitle("Reminder Days", displayMode: .inline)
.navigationBarItems(trailing: Button(action: {
print("getting the date interval from the nav button")
//self.daysSincePhoto = self.userUtilitiesVM.getOldSchool()
self.daysSincePhoto = self.userUtilitiesVM.getFromEnvironment()
//self.daysSincePhoto = self.getFromEnvironment()
} , label: { Text("Fetch")
}))
}
}
//this always works
func getFromEnvironment() -> Int {
let numberOfRecords = self.dermPhotos.count
if numberOfRecords > 0 {
let now = Date()
let lastDate = self.dermPhotos.last?.addDate
//ok to bang - addDate is always added to core data
let dateInterval = DateInterval(start: lastDate!, end: now)
let days = Int(dateInterval.duration) / (24 * 3600)
self.daysSincePhoto = days
return days
}
return 0
}
}
And this is the ViewModel:
class UserUtilitiesViewModel: ObservableObject {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: DermPhoto.getAllDermPhotos()) var dermPhotos: FetchedResults<DermPhoto>
#Published var dateInterval: Int = 50
//Comment 2 this always works
func getOldSchool() -> Int {
let kAppDelegate = UIApplication.shared.delegate as! AppDelegate
let context = kAppDelegate.persistentContainer.viewContext
var resultsDermPhotos : [DermPhoto] = []
let fetchRequest: NSFetchRequest<DermPhoto> = DermPhoto.fetchRequest() as! NSFetchRequest<DermPhoto>
let sortDescriptor = NSSortDescriptor(key: "addDate", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
do {
resultsDermPhotos = try context.fetch(fetchRequest)
} catch {
print("the fetchRequest error is \(error.localizedDescription)")
}
let numberOfRecords = resultsDermPhotos.count
if numberOfRecords > 0 {
let now = Date()
let lastDate = resultsDermPhotos.last?.addDate
//ok to bang this since addDate is always added to core data
let di = DateInterval(start: lastDate!, end: now)
let days = Int(di.duration) / (24 * 3600)
dateInterval = days
return days
}
return 0
}
//this never works
func getFromEnvironment() -> Int {
//Comment 1 - this breaks
let numberOfRecords = self.dermPhotos.count
if numberOfRecords > 0 {
let now = Date()
let lastDate = self.dermPhotos.last?.addDate
//ok to bang this since addDate is always added to core data
let di = DateInterval(start: lastDate!, end: now)
let days = Int(di.duration) / (24 * 3600)
dateInterval = days
return days
}
return 0
}
}
Clearly I can use old school or abandon the ViewModel idea but I would like to know how to fix this. Any guidance would be appreciated. Xcode 11.3 (11C29)

I don't think you can access #Environment from outside View hierarchy.
Your view model does not conform to view. But it won't be view model once it conforms to it.
=> you should abandon view model idea

Related

Set array elements based on current time

I currently have the code below:
var arrayEX = ["8:00 - 11:00", "11:00 - 2:00", "2:00 - 5:00"]
#State var selectedtime = ""
Picker("Start time", selection: $selectedtime) {
ForEach(arrayEX, id: \.self) {
Text($0)
}
}
.padding()
.pickerStyle(SegmentedPickerStyle())
My question is; How can I not show the 1st element of the array after 8:00am, the 2nd element after 11:00am, and the 3rd element after 2:00pm with a message saying there are no times left today?
you could try this approach, where the start times are converted to minutes and then filtered according to the current time.
struct ContentView: View {
// -- here, note 24h clock and consistent pattern `hh:mm`
var arrayEX = ["08:00 - 11:00", "11:00 - 14:00", "14:00 - 17:00"]
#State var selectedtime = ""
#State var timesLeft: [String] = [] // <-- here
var body: some View {
Picker("Start time", selection: $selectedtime) {
ForEach(timesLeft, id: \.self) { // <-- here timesLeft
Text($0)
}
}
.padding()
.pickerStyle(SegmentedPickerStyle())
.onAppear {
let hour = Calendar.current.component(.hour, from: .now)
let minute = Calendar.current.component(.minute, from: .now)
let minutesNow = (Int(hour)*60) + Int(minute) // <-- minutes since the start of day
// convert start times to minutes
let startTimes: [Int] = arrayEX.map{
let h = String($0.prefix(2))
let m = String($0.dropFirst(3).prefix(2))
return ((Int(h) ?? 0) * 60) + (Int(m) ?? 0)
}
// find the index in arrayEX where we are past the time
if let ndx = startTimes.firstIndex(where: { $0 > minutesNow }) {
timesLeft = Array(arrayEX.suffix(from: ndx))
} else {
timesLeft = ["no times left today"]
}
}
}
}

In SwiftUI how do I animate changes one at a time when they occur in a called method?

Although I get an animation when I tap the button, it's not the animation I want.
The entire view is being replaced at once, but I want to see each element change in sequence. I tried in both the parent view and in the called method. Neither produces the desired result.
(this is a simplified version of the original code)
import SwiftUI
struct SequencedCell: Identifiable {
let id = UUID()
var value: Int
mutating func addOne() {
value += 1
}
}
struct AQTwo: View {
#State var cells: [SequencedCell]
init() {
_cells = State(initialValue: (0 ..< 12).map { SequencedCell(value: $0) })
}
var body: some View {
VStack {
Spacer()
Button("+") {
sequencingMethod(items: $cells)
}
.font(.largeTitle)
Spacer()
HStack {
ForEach(Array(cells.enumerated()), id: \.1.id) { index, item in
// withAnimation(.linear(duration: 4)) {
Text("\(item.value)").tag(index)
// }
}
}
Spacer()
}
}
func sequencingMethod(items: Binding<[SequencedCell]>) {
for cell in items {
withAnimation(.linear(duration: 4)) {
cell.wrappedValue = SequencedCell(value: cell.wrappedValue.value + 1)
// cell.wrappedValue.addOne()
}
}
}
}
struct AQTwoPreview: PreviewProvider {
static var previews: some View {
AQTwo()
}
}
So I want the 0 to turn into a 1, the 1 then turn into a 2, etc.
Edit:
Even though I have accepted an answer, it answered my question, but didn't solve my issue.
I can't use DispatchQueue.main.asyncAfter because the value I am updating is an inout parameter and it makes the compiler unhappy:
Escaping closure captures 'inout' parameter 'grid'
So I tried Malcolm's (malhal) suggestion to use delay, but everything happens immediately with no sequential animation (the entire block of updated items animate as one)
Here's the recursive method I am calling:
static func recursiveAlgorithm(targetFill fillValue: Int, in grid: inout [[CellItem]],
at point: (x: Int, y: Int), originalFill: Int? = nil, delay: TimeInterval) -> [[CellItem]] {
/// make sure the point is on the board (or return)
guard isValidPlacement(point) else { return grid }
/// the first time this is called we don't have `originalFill`
/// so we read it from the starting point
let tick = delay + 0.2
//AnimationTimer.shared.tick()
let startValue = originalFill ?? grid[point.x][point.y].value
if grid[point.x][point.y].value == startValue {
withAnimation(.linear(duration: 0.1).delay(tick)) {
grid[point.x][point.y].value = fillValue
}
_ = recursiveAlgorithm(targetFill: fillValue, in: &grid, at: (point.x, point.y - 1), originalFill: startValue, delay: tick)
_ = recursiveAlgorithm(targetFill: fillValue, in: &grid, at: (point.x, point.y + 1), originalFill: startValue, delay: tick)
_ = recursiveAlgorithm(targetFill: fillValue, in: &grid, at: (point.x - 1, point.y), originalFill: startValue, delay: tick)
_ = recursiveAlgorithm(targetFill: fillValue, in: &grid, at: (point.x + 1, point.y), originalFill: startValue, delay: tick)
}
return grid
}
Further comments/suggestions are welcome, as I continue to wrestle with this.
As mentioned in the comments, the lowest-tech version is probably just using a DisatpchQueue.main.asyncAfter call:
func sequencingMethod(items: Binding<[SequencedCell]>) {
var wait: TimeInterval = 0.0
for cell in items {
DispatchQueue.main.asyncAfter(deadline: .now() + wait) {
withAnimation(.linear(duration: 1)) {
cell.wrappedValue = SequencedCell(value: cell.wrappedValue.value + 1)
}
}
wait += 1.0
}
}
You could use delay(_:) for that, e.g.
func sequencingMethod(items: Binding<[SequencedCell]>) {
var delayDuration = 0.0
for cell in items {
withAnimation(.linear(duration: 4).delay(delayDuration)) {
cell.wrappedValue = SequencedCell(value: cell.wrappedValue.value + 1)
}
delayDuration += 0.5
}
}

How to create a dynamic calculation based off more than 1 State

I am brand new to Swift (and coding in general). I am working on an app that will output a calculation based off the tracking of two states. The two states are brewModel and waterAmount. I am able to successfully create a function that will return one calculation based on the two states. However, now I am trying to create a Picker that will toggle the calculation between two measurements - grams and tablespoons. This is where I am having trouble.
I tried to write a series of conditionals in different ways such as if and else if as well as switch cases, but it doesn't work. When I build the simulator, Xcode will just think for a long time until I stop it. Sometimes I get error messages after I manually stop it and sometimes I don't. Today I got "Command CompileSwiftSources failed with a nonzero exit code."
Does anyone have any suggestions?
Also, I apologize if my code is messy, I have a bunch of things commented out that I am playing with. The func computeGrinds does work but just for the one calculation. Thank you!
import SwiftUI
struct Water: View {
// #EnvironmentObject var favorites: Favorites
#State var animationInProgress = true
#State var brewModel: BrewModel
#State var waterAmount: Int = 1
#State var grindsSelection = "tbsp"
var grindOptions = ["tbsp", "grams"]
// var resultGrindCalc: Double {
//
// var value = Double(0)
// }
// switch grindsSelection {
// case "tbsp" || brewModel.frenchPress:
// value = Double(waterAmount) * 2.5
//
// }
//
// func computeGrinds () -> Double {
// switch brewModel {
// case .frenchPress, .chemex:
// return (2.5 * Double(waterAmount))
// case .drip :
// return Double(2 * Double(waterAmount))
// case .mokaPot:
// return Double(1 * Double(waterAmount))
// case .aeroPress:
// return Double(1.6 * Double(waterAmount))
// // default:
// // return(1 * Double(waterAmount))
// }
// }
var body: some View {
VStack (spacing: 5) {
Spacer()
HStack {
// Text("").padding(20)
Text("How many cups do you want to brew?")
Picker("", selection: $waterAmount) {
ForEach(1...15, id: \.self){
Text("\($0)")
}
}
// Spacer()
}.padding()
.overlay (
RoundedRectangle(cornerRadius: 16)
.stroke(Color("Custom Color"), lineWidth: 8)
)
// gif/image conditionals
if (brewModel == .frenchPress) {
LottieView(name: "frenchpress", loopMode: .loop)
} else if brewModel == .chemex {
LottieView(name: "pourover", loopMode: .loop)
} else if brewModel == .aeroPress {
LottieView(name: "aeropress", loopMode: .loop)
} else if brewModel == .mokaPot {
LottieView(name: "mokapot", loopMode: .loop)
} else if brewModel == .drip {
Image("Drip")
.resizable()
.scaledToFit()
}
// I would have more conditionals but testing with just these two for now
var testingCalcCond = Double
if (brewModel == .frenchPress)||(grindsSelection=="tbsp") {
testingCalcCond = (2.5 * Double(waterAmount))
} else if (brewModel == .frenchPress)||(grindsSelection=="grams") {
testingCalcCond = (16 * Double(waterAmount))
}
let formatted = String(format: "%.2f", testingCalcCond)
// let formatted = String(format: "%.2f", computeGrinds())
HStack {
Text("**\(formatted)**")
Picker("Select Grinds Units: ", selection: $grindsSelection, content: {
ForEach(grindOptions, id: \.self) {
Text($0)
}
}).onChange(of: grindsSelection) { _ in computeGrinds() }
Text("of coffee grinds needed")
}
.padding()
.overlay (
RoundedRectangle(cornerRadius: 16)
.stroke(Color("Custom Color"), lineWidth: 8)
)
}
Spacer()
}
}
struct Water_Previews: PreviewProvider {
static var previews: some View {
Water(brewModel: .drip)
}
}
}
*I'm using Xcode 13.2.1
*I'm using swiftUI
There are 2 aspects you want to think over:
1. How to update the result value based on the inputs.
Your result value is based on two inputs: brewModel and waterAmount. Both are #State vars and changed by a picker.
I changed your computeGrinds func to a computed property, because this will be automatically called when one of the two base values changes. Then there is no need for .onchange anymore, you can just use the var value – it will always be up to date.
2. recalculating from tbsp to grams.
This is more of a math thing: As I understand, for .frenchPress you need either 2.5 tbsp – or 16 grams per cup. So 1 tbsp = 16 / 2.5 = 6.4 grams. Once you know that you just have to go through the switch case once, and use the unitValue to recalculate. I integrated that too ;)
Here is my simplified code:
enum BrewModel {
case frenchPress
case chemex
case drip
case mokaPot
case aeroPress
}
struct ContentView: View {
#State var animationInProgress = true
#State var brewModel: BrewModel = .frenchPress
#State var waterAmount: Int = 1
#State var grindsSelection = "tbsp"
let grindOptions = ["tbsp", "grams"]
// computed var instead of func, does the same
var computeGrinds: Double {
// transforms tbsp = 1 to grams (= 6.4 ?)
var unitValue: Double = 1.0
if grindsSelection == "grams" {
unitValue = 6.4
}
switch brewModel {
case .frenchPress, .chemex:
return (2.5 * unitValue * Double(waterAmount))
case .drip :
return Double(2 * unitValue * Double(waterAmount))
case .mokaPot:
return Double(1 * unitValue * Double(waterAmount))
case .aeroPress:
return Double(1.6 * unitValue * Double(waterAmount))
}
}
var body: some View {
VStack (spacing: 5) {
HStack {
Text("How many cups do you want to brew?")
Picker("", selection: $waterAmount) {
ForEach(1...15, id: \.self){
Text("\($0)")
}
}
}
.padding()
.overlay (
RoundedRectangle(cornerRadius: 16)
.stroke(Color.brown, lineWidth: 8)
)
.padding(.bottom)
let formatted = String(format: "%.2f", computeGrinds)
HStack {
Text("**\(formatted)**")
Picker("Select Grinds Units: ", selection: $grindsSelection, content: {
ForEach(grindOptions, id: \.self) {
Text($0)
}
})
Text("of coffee grinds needed")
}
.padding()
.overlay (
RoundedRectangle(cornerRadius: 16)
.stroke(Color.brown, lineWidth: 8)
)
}
}
}

Animating the letters in a word w the app opens - SwiftUI

I'm trying to get a dancing letters effect when my app first opens.
I'm close. The coding below almost does what I want. I use a ForEach loop to loop through the letters of the word and apply an animation to each letter. And I use the onAppear function to set the drag amount when the app opens.
With this coding I can get the 'forward' motion but I can't get the animation to reverse so that the letters end up in their original position. I've tried adding a repeat with reverse, but, again, the letters never return to their original position
Does anyone have any idea how to do this?
struct ContentView: View {
let letters = Array("Math Fun!")
#State private var enabled = false
#State private var dragAmount = CGSize.zero
var body: some View {
HStack(spacing: 0) {
ForEach(0..<letters.count) { num in
Text(String(self.letters[num]))
.padding(5)
.font(.title)
.background(self.enabled ? Color.blue : Color.red)
.offset(self.dragAmount)
.animation(Animation.default.delay(Double(num)/20).repeatCount(3, autoreverses: true))
}
}
.onAppear {
self.dragAmount = CGSize(width: 0, height: 80)
self.enabled.toggle()
}
}
}
Update: with Xcode 13.4 / iOS 15.5
Animation is based on changed states, we switched states and view animated to the new states, so to rollback we need to switch the states back.
Here is the possible approach (might still require tuning, but is ok for demo)
struct ContentView: View {
let letters = Array("Math Fun!")
#State private var enabled = false
#State private var dragAmount = CGSize.zero
var body: some View {
HStack(spacing: 0) {
ForEach(0..<letters.count, id: \.self) { num in
Text(String(self.letters[num]))
.padding(5)
.font(.title)
.background(self.enabled ? Color.blue : Color.red)
.offset(self.dragAmount)
.animation(Animation.default.delay(Double(num)/20), value: enabled)
}
}
.onAppear {
self.dragAmount = CGSize(width: 0, height: 80)
self.enabled.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.dragAmount = .zero
self.enabled.toggle()
}
}
}
}
You can use AnimatableModifier to achieve this effect. here is a sample code:
extension Double {
var rad: Double { return self * .pi / 180 }
var deg: Double { return self * 180 / .pi }
}
struct ContentView: View {
#State private var flag = false
var body: some View {
VStack {
Spacer()
Color.clear.overlay(WaveText("Your Text That Need Animate", waveWidth: 6, pct: flag ? 1.0 : 0.0).foregroundColor(.blue)).frame(height: 40)
Spacer()
}.onAppear {
withAnimation(Animation.easeInOut(duration: 2.0).repeatForever()) {
self.flag.toggle()
}
}
}
}
struct WaveText: View {
let text: String
let pct: Double
let waveWidth: Int
var size: CGFloat
init(_ text: String, waveWidth: Int, pct: Double, size: CGFloat = 34) {
self.text = text
self.waveWidth = waveWidth
self.pct = pct
self.size = size
}
var body: some View {
Text(text).foregroundColor(Color.clear).modifier(WaveTextModifier(text: text, waveWidth: waveWidth, pct: pct, size: size))
}
struct WaveTextModifier: AnimatableModifier {
let text: String
let waveWidth: Int
var pct: Double
var size: CGFloat
var animatableData: Double {
get { pct }
set { pct = newValue }
}
func body(content: Content) -> some View {
HStack(spacing: 0) {
ForEach(Array(text.enumerated()), id: \.0) { (n, ch) in
Text(String(ch))
.scaleEffect(self.effect(self.pct, n, self.text.count, Double(self.waveWidth)))
}
}
}
func effect(_ pct: Double, _ n: Int, _ total: Int, _ waveWidth: Double) -> CGFloat {
let n = Double(n)
let total = Double(total)
return CGFloat(1 + valueInCurve(pct: pct, total: total, x: n/total, waveWidth: waveWidth))
}
func valueInCurve(pct: Double, total: Double, x: Double, waveWidth: Double) -> Double {
let chunk = waveWidth / total
let m = 1 / chunk
let offset = (chunk - (1 / total)) * pct
let lowerLimit = (pct - chunk) + offset
let upperLimit = (pct) + offset
guard x >= lowerLimit && x < upperLimit else { return 0 }
let angle = ((x - pct - offset) * m)*360-90
return (sin(angle.rad) + 1) / 2
}
}
}
You can find refrence and compelete answer here

Method collectionView (didSelectItemAt...) doesn't Work at iTunesConnect; However, It does Work on Debug

I've updated my Xcode to the latest version, currently it's 10.2.1(10E1001) and migrated my project from Swift 4 to Swift 5.
It made me some troubles, but finally I've built my project and it works correctly from debug version on my iPhone.
After that I've had few troubles with archiving my project (maybe it could be a reason)
I've upload it in App Store and after that tried my app at TestFlight.
Plus, for some reason few code in my project works wrong.
It seems like collectionView(didSelectItemAtIndexPath...) doesn't work (but it perfectly works in Xcode) and my custom layout of collectionView doesn't work too (but also works on Debug).
It seems like layout works wrong, but I can't understand what's the difference between Debug and Release version except provisioning profile.
I can share you more videos, code, w/e you need, I really need to resolve this issue.
I've not found anything else like that in the web
I've taken that custom layout code from here https://codereview.stackexchange.com/questions/197017/page-and-center-uicollectionview-like-app-store
class SnapPagingLayout: UICollectionViewFlowLayout {
private var centerPosition = true
private var peekWidth: CGFloat = 0
private var indexOfCellBeforeDragging = 0
convenience init(centerPosition: Bool = true, peekWidth: CGFloat = 40, spacing: CGFloat? = nil, inset: CGFloat? = nil) {
self.init()
self.scrollDirection = .horizontal
self.centerPosition = centerPosition
self.peekWidth = peekWidth
if let spacing = spacing {
self.minimumLineSpacing = spacing
}
if let inset = inset {
self.sectionInset = UIEdgeInsets(top: 0, left: inset, bottom: 0, right: inset)
}
}
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
self.itemSize = calculateItemSize(from: collectionView.bounds.size)
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
guard let collectionView = collectionView,
!newBounds.size.equalTo(collectionView.bounds.size) else {
return false
}
itemSize = calculateItemSize(from: collectionView.bounds.size)
return true
}
}
private extension SnapPagingLayout {
func calculateItemSize(from bounds: CGSize) -> CGSize {
return CGSize(
width: bounds.width - peekWidth * 2,
height: (bounds.width - peekWidth * 2) / 1.77
)
}
func indexOfMajorCell() -> Int {
guard let collectionView = collectionView else { return 0 }
let proportionalOffset = collectionView.contentOffset.x
/ (itemSize.width + minimumLineSpacing)
return Int(round(proportionalOffset))
}
}
extension SnapPagingLayout {
func willBeginDragging() {
indexOfCellBeforeDragging = indexOfMajorCell()
}
func willEndDragging(withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard let collectionView = collectionView else { return }
// Stop scrollView sliding
targetContentOffset.pointee = collectionView.contentOffset
// Calculate where scrollView should snap to
let indexOfMajorCell = self.indexOfMajorCell()
guard let dataSourceCount = collectionView.dataSource?.collectionView(collectionView, numberOfItemsInSection: 0),
dataSourceCount > 0 else {
return
}
// Calculate conditions
let swipeVelocityThreshold: CGFloat = 0.3 // After some trail and error
let hasEnoughVelocityToSlideToTheNextCell = indexOfCellBeforeDragging + 1 < dataSourceCount && velocity.x > swipeVelocityThreshold
let hasEnoughVelocityToSlideToThePreviousCell = indexOfCellBeforeDragging - 1 >= 0 && velocity.x < -swipeVelocityThreshold
let majorCellIsTheCellBeforeDragging = indexOfMajorCell == indexOfCellBeforeDragging
let didUseSwipeToSkipCell = majorCellIsTheCellBeforeDragging
&& (hasEnoughVelocityToSlideToTheNextCell || hasEnoughVelocityToSlideToThePreviousCell)
guard didUseSwipeToSkipCell else {
// Better way to scroll to a cell
collectionView.scrollToItem(
at: IndexPath(row: indexOfMajorCell, section: 0),
at: centerPosition ? .centeredHorizontally : .left, // TODO: Left ignores inset
animated: true
)
return
}
let snapToIndex = indexOfCellBeforeDragging + (hasEnoughVelocityToSlideToTheNextCell ? 1 : -1)
var toValue = CGFloat(snapToIndex) * (itemSize.width + minimumLineSpacing)
if centerPosition {
// Back up a bit to center
toValue = toValue - peekWidth + sectionInset.left
}
// Damping equal 1 => no oscillations => decay animation
UIView.animate(
withDuration: 0.3,
delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: velocity.x,
options: .allowUserInteraction,
animations: {
collectionView.contentOffset = CGPoint(x: toValue, y: 0)
collectionView.layoutIfNeeded()
},
completion: nil
)
}
}
I wanna see page and center collection view like in App Store. And also I wanna make my didSelect-method work correctly.
This is a bug for Swift 5.0 compiler related to this references:
https://bugs.swift.org/browse/SR-10257
.
Update:
Further searching found an temporary answer at this link on Stackoverflow
You can work around it by explicitly tagging it with #objc for now.

Resources