SwiftUI - how to cancel the default list animation - ios

Is there a way to remove the default list row collapse animation
without using .animation(nill) modifier on the list itself?
as you can see in the clip below, I've implemented an animation modifier on the list, but the default list collapse animation kinda disrupts the desired animation.
https://gfycat.com/cheerywelloffalleycat
I've updated the code below so you could run it on your Xcode without any dependencies.
import SwiftUI
struct CurrencyComparison: View {
#State var mainArray = ["10", "10","10", "10", "10", "10", "10", "10", "10", "10"]
#State var array = ["10", "10","10", "10", "10", "10", "10", "10", "10", "10"]
#State var secondArray = ["20", "20","20", "20", "20", "20", "20", "20", "20", "20"]
#State var hide = false
#State var direction = false
#State var triggerAnimation: Bool
var body: some View {
VStack {
List (self.mainArray, id:\.self) { item in
Text(item)
.foregroundColor(Color.black)
.frame(width: 40, height: 80)
.padding(.leading, 80)
.isHidden(self.hide)
Spacer()
Text(item)
.foregroundColor(Color.black)
.frame(width: 40, height: 40)
.padding(.trailing, 80)
.isHidden(self.hide)
}
.background(LinearGradient(gradient: Gradient(colors: [Color.blue, Color.red]), startPoint: .center, endPoint: .center))
.animation(Animation.spring().delay(triggerAnimation ? 0 : 0.4))
.transition(.asymmetric(insertion: AnyTransition.opacity.combined(with: .move(edge: .trailing)), removal: .move(edge: .trailing)))
.cornerRadius(30)
.padding(.top, 30)
.padding(.bottom, 30)
.shadow(radius: 10)
.gesture(
DragGesture(minimumDistance: 50)
.onEnded { value in
self.hide.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() ) {
if self.mainArray == self.array {
self.mainArray = self.secondArray
} else {
self.mainArray = self.array
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.hide.toggle()
}
}
)
}
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height + 50, alignment: .top)
.background(Color.gray.aspectRatio(contentMode: .fill))
.padding(.top, 120)
}
}
struct CurrencyComparison_Previews: PreviewProvider {
#State static var staticBool = true
static var previews: some View {
CurrencyComparison(triggerAnimation: true)
}
}
extension View {
func isHidden(_ bool: Bool) -> some View {
modifier(HiddenModifier(isHidden: bool))
}
}
private struct HiddenModifier: ViewModifier {
fileprivate let isHidden: Bool
fileprivate func body(content: Content) -> some View {
Group {
if isHidden {
content.hidden()
} else {
content
.transition(.asymmetric(insertion: AnyTransition.opacity.combined(with: .slide), removal: .move(edge: .trailing)))
}
}
}
}

Okay so I've managed to find a solution that works. By changing the opacity of the list items when the list data source change happens.
Setting it to 0 when the undesired default animation play, and bringing it back to 1 when my the desired animations start playing.
That way I can hide the default list animation without removing my own desired animations :)
import SwiftUI
struct CurrencyComparison: View {
#State var mainArray = ["10", "10","10", "10", "10", "10", "10", "10", "10", "10"]
#State var array = ["10", "10","10", "10", "10", "10", "10", "10", "10", "10"]
#State var secondArray = ["20", "20","20", "20", "20", "20", "20", "20", "20", "20"]
#State var toggleHide = false
#State var direction = false
#State var triggerAnimation: Bool
var body: some View {
VStack {
List (self.mainArray, id:\.self) { item in
Text(item)
.foregroundColor(Color.black)
.frame(width: 40, height: 80)
.padding(.leading, 80)
.isHidden(self.toggleHide)
.opacity(self.toggleHide ? 0 : 1)
Spacer()
Text(item)
.foregroundColor(Color.black)
.frame(width: 40, height: 40)
.padding(.trailing, 80)
.isHidden(self.toggleHide)
.opacity(self.toggleHide ? 0 : 1)
}
.background(LinearGradient(gradient: Gradient(colors: [Color.blue, Color.red]), startPoint: .center, endPoint: .center))
.animation(Animation.spring().delay(triggerAnimation ? 0 : 0.4))
.transition(.asymmetric(insertion: AnyTransition.opacity.combined(with: .move(edge: .trailing)), removal: .move(edge: .trailing)))
.cornerRadius(30)
.padding(.top, 30)
.padding(.bottom, 30)
.shadow(radius: 10)
.gesture(
DragGesture(minimumDistance: 50)
.onEnded { value in
self.toggleHide.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
if self.mainArray == self.array {
self.mainArray = self.secondArray
} else {
self.mainArray = self.array
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.9) {
self.toggleHide.toggle()
}
}
)
}
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height + 50, alignment: .top)
.background(Color.gray.aspectRatio(contentMode: .fill))
.padding(.top, 120)
}
}
struct CurrencyComparison_Previews: PreviewProvider {
#State static var staticBool = true
static var previews: some View {
CurrencyComparison(triggerAnimation: true)
}
}
extension View {
func isHidden(_ bool: Bool) -> some View {
modifier(HiddenModifier(isHidden: bool))
}
}
private struct HiddenModifier: ViewModifier {
fileprivate let isHidden: Bool
fileprivate func body(content: Content) -> some View {
Group {
if isHidden {
content.hidden()
} else {
content
.transition(.asymmetric(insertion: AnyTransition.opacity.combined(with: .slide), removal: .move(edge: .trailing)))
}
}
}
}

Related

Change button text on last image of Slider in SwiftUI

I have an image slider with dot indicator named "Onboardingslider". I have used it in my another screen "onboard4" , I want to change the text of "next" button to "get started" only on last image of slider otherwise it stays "next".
I have tried a lot of things but nothing works
Please help me I'm a newbie
struct onboard4: View {
#State var showModal = false
#State var maxlogoheight: CGFloat = 0
#State var isLinkActive = false
var body: some View {
NavigationView {
ZStack{
//max height will be width of the screen
GeometryReader{ proxy -> AnyView in
let height = proxy.frame(in: .global).height
DispatchQueue.main.async {
if maxlogoheight == 0 {
maxlogoheight = height
}
}
return AnyView (
ZStack{
Image("Logo")
.resizable()
.scaledToFit()
.offset(x: getReact().width/3.5, y: -height/1.25)
}//zstack 2
// .padding(.leading,10)
)//anyview
}//end of gr
.frame(maxHeight: getReact().width)
VStack{
Onboardingslider()
Button(action: {
showModal = true
}) {
ZStack{
Text("Next")
.foregroundColor(Color.white)
.fontWeight(.bold)
.frame (width: 295, height: 30)
.padding()
.background(Color("YellowAccent"))
.cornerRadius(20)
.shadow(color: .gray, radius: 5, x: 0, y: 4)
Image("NextButtonOnboard")
}
}
.fullScreenCover(isPresented: $showModal) {
LoginView() }
.offset(y: getSafearea().bottom + -55)
}//vstack
Button(action: {}, label: {
Text("Continue to the listing service")
.underline()
})
.foregroundColor(Color.black)
.offset(y: getSafearea().bottom + 310)
}//zstack
.background(
NavigationLink(destination: LoginView(), isActive: $isLinkActive) {
EmptyView()
}
.hidden()
)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showModal = true
}) {
Text("Skip").underline()
}
.foregroundColor(Color("YellowAccent"))
.font(.system(size: 20,weight: .semibold,design: .serif))
.frame(width: 100, height: 100)
.padding(.top)
.fullScreenCover(isPresented: $showModal) {
LoginView() }
//.padding(.bottom)
}//toolbaritem
}//toolbar
}
}
}
struct onboard4_Previews: PreviewProvider {
static var previews: some View {
onboard4()
}
}
struct Onboardingslider: View {
private let images = ["1", "2", "3", "4"]
init() {
// modify appearance
UIPageControl.appearance().currentPageIndicatorTintColor = .orange
UIPageControl.appearance().pageIndicatorTintColor = .gray
}
var body: some View {
TabView {
ForEach(images, id: \.self) { item in
Image(item)
.padding(.leading,24)
}
}
.frame(height: 600)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
}
}

I can't modify circular progress bar trim in SwiftUI

I'm making a math game and I'm trying to implement a circular progress bar, where I get errors. I'm not sure if I'm doing anything wrong, but I can't adapt it to the progress of the game, and that's what I want to do. Please look over my code down below...
struct QuestionView: View {
#State var show = false
#State var showSheet: Bool = false
#State var showSheetA: Bool = false
#State var showSheet2: Bool = false
#State private var correctAnswer = 0
#State private var choiceArray: [Int] = [0, 1, 2, 3]
#State private var firstNumber = 0
#State private var secondNumber = 0
#State private var difficulty = 100
#State private var score = 0
#State private var Background1 = "Background 1"
#State private var Background2 = "Background 2"
#State private var Background3 = "Background 3"
#State private var Background4 = "Background 4"
#State private var Background5 = "Background 5"
#State private var Background6 = "Background 6"
#State private var Background7 = "Background 7"
#State private var Background8 = "Background 8"
#State private var Background9 = "Background 9"
#State private var Background10 = "Background 10"
#State private var Background11 = "Background 11"
#State private var Background12 = "Background 12"
#State private var background = [
"Background 1",
"Background 2",
"Background 3",
"Background 4",
"Background 5",
"Background 6",
"Background 7",
"Background 8",
"Background 9",
"Background 10",
"Background 11",
"Background 12",
]
#State var value = "0"
let buttons: [[CalcButton]] = [
[.clear, .negative, .percent, .divide],
[.seven, .eight, .nine, .multiply],
[.four, .five, .six, .subtract],
[.one, .two, .three, .add],
[.zero, .decimal, .equal]
]
#State var currentOperation: Operation = .none
#State var runningNumber = 0
var body: some View {
ZStack {
Image("\(Background1)")
.resizable()
.aspectRatio(contentMode: .fill)
.edgesIgnoringSafeArea(.all)
.blur(radius: 20)
.onAppear {
shuffle()
}
VStack {
VStack(alignment: .leading) {
HStack {
HStack {
Circle()
//Where the problem is happening at the trim
.trim(from: CGFloat(show ? Int(0.99) : answerIsCorrect(answer: correctAnswer)), to: 0.01)
.stroke(LinearGradient(gradient: Gradient(colors: [Color.white, Color.white.opacity(0.2)]), startPoint: .topLeading, endPoint: .bottomTrailing), style: StrokeStyle(lineWidth: 17.5, lineCap: .round))
.shadow(radius: 8)
.rotationEffect(.degrees(90))
.rotation3DEffect(Angle(degrees: 180), axis: (x: 1, y: 0, z: 0))
.frame(width: 60, height: 60)
.animation(.easeOut)
.padding()
.padding([.top, .leading])
Spacer()
}
Button {
showSheet.toggle()
} label: {
VStack {
ZStack {
Rectangle()
.fill(.thinMaterial)
.frame(width: 80, height: 40)
.cornerRadius(30)
.padding(.top, 13)
.padding(.trailing, 0)
HStack {
Image(systemName: "circle.grid.3x3.fill")
.foregroundColor(.white)
.font(.title3)
.padding(.top, 13)
.padding(.trailing, 0)
}
}
Text("CALCULATOR")
.font(.system(size: 11))
.padding(.trailing, 0)
}
}
.halfSheet(showSheet: $showSheet) {
ZStack {
Color.black.opacity(0.925).ignoresSafeArea()
VStack {
Spacer()
// Text display
HStack {
Spacer()
Text(value)
.bold()
.font(.system(size: 70))
.foregroundColor(.white)
.minimumScaleFactor(0.5)
}
.padding(.leading)
.padding([.top, .trailing], 23)
.padding(.bottom, 2)
// Our Buttons
ForEach(buttons, id: \.self) { row in
HStack(spacing: 12) {
ForEach(row, id: \.self) { item in
Button {
self.didTap(button: item)
} label: {
Text(item.rawValue)
.font(.system(size: 36))
.frame(width: self.buttonWidth(item: item), height: 55)
.scaledToFit()
.background(item.buttonColor)
.foregroundColor(.white)
.cornerRadius(95)
}
}
}
.padding(.bottom, 0.55)
.padding([.leading, .trailing], 20)
}
}
}
}
Button {
showSheet2.toggle()
} label: {
VStack {
ZStack {
Rectangle()
.fill(.thinMaterial)
.frame(width: 80, height: 40)
.cornerRadius(30)
.padding(.top, 13)
.padding(.trailing, 28.5)
HStack {
Image(systemName: "book")
.foregroundColor(.white)
.font(.title3)
.padding(.top, 13)
.padding(.trailing, 28.5)
}
}
Text("DICTIONARY")
.font(.system(size: 11))
.padding(.trailing, 25.5)
}
}
.halfSheet(showSheet: $showSheet2) {
ZStack {
Home()
}
}
}
.padding(.top, 50)
}
VStack {
Text("Solve the following:")
.foregroundColor(.white)
.font(.title2)
.fontWeight(.medium)
.padding(.bottom, 24)
Text("\(firstNumber) + \(secondNumber)")
.foregroundColor(.white)
.font(.system(size: 60, design: .rounded))
.fontWeight(.medium)
.padding(.bottom, 28)
.onAppear {
generateAnswers()
}
VStack(alignment: .center, spacing: 0.01) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 150), spacing: 2.5)]) {
ForEach(0..<4, id: \.self) { index in
Button {
answerIsCorrect(answer: choiceArray[index])
generateAnswers()
} label: {
AnswerButton(number: choiceArray[index])
}
}
}
Spacer()
}
HStack {
Button {
} label: {
ZStack {
Rectangle()
.fill(Color.white.opacity(0.5))
.frame(maxWidth: 150, maxHeight: 50)
.cornerRadius(15)
.padding([.leading, .trailing])
Image(systemName: "chevron.left")
.font(.title)
}
}
Button {
showSheetA.toggle()
} label: {
VStack {
ZStack {
Rectangle()
.fill(Color.white.opacity(0.5))
.frame(maxWidth: 150, maxHeight: 50)
.cornerRadius(15)
.padding([.leading, .trailing])
Image(systemName: "xmark")
.font(.title)
.foregroundColor(.red)
}
}
}
ZStack {
Rectangle()
.fill(Color.white.opacity(0.5))
.frame(maxWidth: 150, maxHeight: 50)
.cornerRadius(15)
.padding([.leading, .trailing])
Image(systemName: "chevron.right")
.font(.title)
}
}
.padding(.trailing, 6)
.padding(.bottom)
Spacer(minLength: 65)
}
}
}
}
func shuffle() {
Background1 = background[Int.random(in: background.indices)]
Background2 = background[Int.random(in: background.indices)]
Background3 = background[Int.random(in: background.indices)]
Background4 = background[Int.random(in: background.indices)]
Background5 = background[Int.random(in: background.indices)]
Background6 = background[Int.random(in: background.indices)]
Background7 = background[Int.random(in: background.indices)]
Background8 = background[Int.random(in: background.indices)]
Background9 = background[Int.random(in: background.indices)]
Background10 = background[Int.random(in: background.indices)]
Background11 = background[Int.random(in: background.indices)]
Background12 = background[Int.random(in: background.indices)]
}
func answerIsCorrect(answer: Int) {
let isCorrect = answer == correctAnswer ? true : false
if isCorrect {
self.score += 1
} else {
self.score -= 1
}
}
func generateAnswers() {
firstNumber = Int.random(in: 0...(difficulty/2))
secondNumber = Int.random(in: 0...(difficulty/2))
var answerList = [Int]()
correctAnswer = firstNumber + secondNumber
for i in 0...2 {
answerList.append(Int.random(in: 0...difficulty))
}
answerList.append(correctAnswer)
choiceArray = answerList.shuffled()
}
func didTap(button: CalcButton) {
switch button {
case .add, .subtract, .multiply, .divide, .equal:
if button == .add {
self.currentOperation = .add
self.runningNumber = Int(self.value) ?? 0
} else if button == .subtract {
self.currentOperation = .subtract
self.runningNumber = Int(self.value) ?? 0
} else if button == .multiply {
self.currentOperation = .multiply
self.runningNumber = Int(self.value) ?? 0
} else if button == .divide {
self.currentOperation = .divide
self.runningNumber = Int(self.value) ?? 0
} else if button == .equal {
let runningValue = self.runningNumber
let currentValue = Int(self.value) ?? 0
switch self.currentOperation {
case .add:
self.value = "\(runningValue + currentValue)"
case .subtract:
self.value = "\(runningValue - currentValue)"
case .multiply:
self.value = "\(runningValue * currentValue)"
case .divide:
self.value = "\(runningValue / currentValue)"
case .none:
break
}
}
if button != .equal {
self.value = "0"
}
case .clear:
self.value = "0"
case .decimal, .percent, .negative:
break
default:
let number = button.rawValue
if self.value == "0" {
value = number
} else {
self.value = "\(self.value)\(number)"
}
}
}
func buttonWidth(item: CalcButton) -> CGFloat {
if item == .zero {
return ((UIScreen.main.bounds.width - (4*12)) / 4) * 2
}
return (UIScreen.main.bounds.width - (5*12)) / 4
}
func buttonHeight() -> CGFloat {
return (UIScreen.main.bounds.height - (5*12)) / 4
}
}
struct AnswerButton: View {
var number: Int
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color.white.opacity(0.6))
.frame(maxWidth: .infinity, minHeight: 110)
.cornerRadius(15)
.padding([.leading, .trailing], 10)
.padding(.top)
.padding(.trailing, 6)
Text("\(number)")
.font(.largeTitle)
.fontWeight(.medium)
.foregroundColor(.primary)
.padding([.leading, .trailing], 10)
.padding(.top)
.padding(.trailing, 6)
}
}
}
enum CalcButton: String {
case one = "1"
case two = "2"
case three = "3"
case four = "4"
case five = "5"
case six = "6"
case seven = "7"
case eight = "8"
case nine = "9"
case zero = "0"
case add = "+"
case subtract = "-"
case divide = "รท"
case multiply = "ร—"
case equal = "="
case clear = "AC"
case decimal = "."
case percent = "%"
case negative = "+/-"
var buttonColor: Color {
switch self {
case .add, .subtract, .multiply, .divide, .equal:
return .orange
case .clear, .negative, .percent:
return .gray
default:
return Color(UIColor(red: 55/255.0, green: 55/255.0, blue: 55/255.0, alpha: 1))
}
}
}
enum Operation {
case add, subtract, multiply, divide, none
}
extension View {
func halfSheet<SheetView: View>(showSheet: Binding<Bool>, #ViewBuilder sheetView: #escaping () -> SheetView) -> some View {
return self
.background(
HalfSheetHelper(sheetView: sheetView(), showSheet: showSheet)
)
}
}
struct HalfSheetHelper<SheetView: View>: UIViewControllerRepresentable {
var sheetView: SheetView
#Binding var showSheet: Bool
class Coordinator {
let dummyController = UIViewController()
let sheetController: CustomHostingController<SheetView>
init(sheetView: SheetView, showSheet: Binding<Bool>) {
sheetController = CustomHostingController(rootView: sheetView, onDismiss: { showSheet.wrappedValue = false })
dummyController.view.backgroundColor = .clear
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(sheetView: sheetView, showSheet: $showSheet)
}
func makeUIViewController(context: Context) -> UIViewController {
return context.coordinator.dummyController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
context.coordinator.sheetController.rootView = sheetView
if showSheet && uiViewController.presentedViewController == nil {
uiViewController.present(context.coordinator.sheetController, animated: true)
}
}
}
class CustomHostingController<Content: View>: UIHostingController<Content> {
var onDismiss: (() -> Void)?
convenience init(rootView: Content, onDismiss: #escaping () -> Void) {
self.init(rootView: rootView)
self.onDismiss = onDismiss
}
override func viewDidLoad() {
if let presentationController = presentationController as? UISheetPresentationController {
presentationController.detents = [
.large(),
.medium()
]
presentationController.prefersGrabberVisible = true
}
}
override func viewDidDisappear(_ animated: Bool) {
onDismiss?()
}
}
struct QuestionView_Previews: PreviewProvider {
static var previews: some View {
QuestionView()
}
}
Thank you for looking at my code. I look forward to your answer!
struct ContentView: View {
#State private var progress: Double = 0.1
var body: some View {
VStack {
Circle()
.trim(from: 0, to: progress)
.stroke(lineWidth: 20)
.rotationEffect(Angle(degrees: 180))
.shadow(radius: 8)
.rotationEffect(.degrees(90))
.frame(width: 60, height: 60)
.padding()
Slider(value: $progress, in: 0...1)
.padding()
}
}
}

Very weird bug where sheet cover is not working in Swift iOS 14.1-ios 14.4 but working for iOS 14.5 and above

this bug has been scratching my head for the past few days and I still don't know why the problem is arising and what the fix is. I have a camera screen and integrated it with the TOCropViewController (https://github.com/TimOliver/TOCropViewController) to allow a user to select a picture from their photo library and crop it to show a new post. For some reason the image picker is detecting that it should change the view to the ImagePicker from the camera view screen but it's not displaying it on ios14.4 and below but it works just fine for iOS 14.5 and above.
Here is my camera view code:
struct CameraView: View {
#StateObject var model = CameraModel()
#State var currentZoomFactor: CGFloat = 1.0
#StateObject var registerData = RegisterViewModel()
#StateObject var newPostData = NewPostModel()
enum SheetType {
case imagePick
case imageCrop
case share
}
#State private var currentSheet: SheetType = .imagePick
#State private var actionSheetIsPresented = false
#State private var sheetIsPresented = false
#State private var originalImage: UIImage?
#State private var image: UIImage?
#State private var croppingStyle = CropViewCroppingStyle.default
#State private var croppedRect = CGRect.zero
#State private var croppedAngle = 0
#StateObject var userData = UserViewModel()
var captureButton: some View {
Button(action: {
let impactMed = UIImpactFeedbackGenerator(style: .light)
impactMed.impactOccurred()
model.capturePhoto()
}, label: {
Circle()
.foregroundColor(.white)
.frame(width: 80, height: 80, alignment: .center)
.overlay(
Circle()
.stroke(Color.black.opacity(0.8), lineWidth: 2)
.frame(width: 65, height: 65, alignment: .center)
)
})
}
var capturedPhotoThumbnail: some View {
Group {
RoundedRectangle(cornerRadius: 10)
.frame(width: 55, height: 55, alignment: .center)
.foregroundColor(Color.gray.opacity(0.2))
.onTapGesture(perform: {
// newPostData.picker.toggle()
self.croppingStyle = .default
self.currentSheet = .imagePick
self.sheetIsPresented = true
print("HERE11 and \(self.currentSheet) and \(self.sheetIsPresented)")
})
.overlay(
Image("gallery")
.renderingMode(.template)
.resizable()
.frame(width: 25, height: 25)
.foregroundColor(Color("white")))
//CODE WITH BUG on ios 14.4 and below. I tried a regular sheet as well that works on another view in ios 14.4 but it doesn't work in the cameraview()
.sheet(isPresented: $sheetIsPresented) {
if (self.currentSheet == .imagePick) {
ImagePickerView(croppingStyle: self.croppingStyle, sourceType: .photoLibrary, onCanceled: {
// on cancel
}) { (image) in
guard let image = image else {
return
}
self.originalImage = image
DispatchQueue.main.async {
self.currentSheet = .imageCrop
self.sheetIsPresented = true
}
}
} else if (self.currentSheet == .imageCrop) {
ZStack {
Color("imagecropcolor").edgesIgnoringSafeArea(.all)
ImageCropView(croppingStyle: self.croppingStyle, originalImage: self.originalImage!, onCanceled: {
// on cancel
}) { (image, cropRect, angle) in
// on success
self.image = image
model.resetPhoto()
newPostData.newPost.toggle()
}
}
}
}
}
}
var flipCameraButton: some View {
Button(action: {
let impactMed = UIImpactFeedbackGenerator(style: .light)
impactMed.impactOccurred()
model.flipCamera()
}, label: {
Circle()
.foregroundColor(Color.gray.opacity(0.2))
.frame(width: 45, height: 45, alignment: .center)
.overlay(
Image(systemName: "camera.rotate.fill")
.foregroundColor(.white))
})
}
var body: some View {
GeometryReader { reader in
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
VStack {
HStack{
Button(action: {
model.switchFlash()
}, label: {
Image(systemName: model.isFlashOn ? "bolt.fill" : "bolt.slash.fill")
.font(.system(size: 20, weight: .medium, design: .default))
})
.accentColor(model.isFlashOn ? .yellow : .white)
.padding(.leading, 30)
Spacer()
if model.photo != nil {
Text("taken photo").onAppear{
newPostData.newPost.toggle()
}
}
// Image(uiImage: model.photo.image!)
// .resizable()
// .aspectRatio(contentMode: .fill)
// .frame(width: 60, height: 60)
// .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
// .animation(.spring())
//
}
CameraPreview(session: model.session)
.gesture(
DragGesture().onChanged({ (val) in
// Only accept vertical drag
if abs(val.translation.height) > abs(val.translation.width) {
// Get the percentage of vertical screen space covered by drag
let percentage: CGFloat = -(val.translation.height / reader.size.height)
// Calculate new zoom factor
let calc = currentZoomFactor + percentage
// Limit zoom factor to a maximum of 5x and a minimum of 1x
let zoomFactor: CGFloat = min(max(calc, 1), 5)
// Store the newly calculated zoom factor
currentZoomFactor = zoomFactor
// Sets the zoom factor to the capture device session
model.zoom(with: zoomFactor)
}
})
)
.onAppear {
model.configure()
}
.alert(isPresented: $model.showAlertError, content: {
Alert(title: Text(model.alertError.title), message: Text(model.alertError.message), dismissButton: .default(Text(model.alertError.primaryButtonTitle), action: {
model.alertError.primaryAction?()
}))
})
.overlay(
Group {
if model.willCapturePhoto {
Color.black
}
}
)
.animation(.easeInOut)
HStack {
capturedPhotoThumbnail
Spacer()
captureButton
.padding(.trailing, 10)
Spacer()
flipCameraButton
}
.padding(.horizontal, 20)
.padding(.bottom, 20)
}
}.fullScreenCover(isPresented: $newPostData.newPost) {
if model.photo == nil {
NewPost(imageData: (self.image?.pngData())! )
} else {
NewPost(imageData: model.photo.originalData)
}
}
}
}
}
Here is where the CameraView() gets called from my Home Screen
import SwiftUI
import Firebase
struct Home: View {
#AppStorage("current_status") var status = false
#AppStorage("showSheet") var showSheet = false
#State var loadedPost = Post(id: 0, PostUID: "", PostName: "", selectedForPost: false, time: Date())
#State var selectedTab = "camera"
var edges = UIApplication.shared.windows.first?.safeAreaInsets
#StateObject var modelData = ModelView()
#StateObject var userData = UserViewModel()
var body: some View {
VStack(spacing: 15){
VStack (spacing: 0) {
GeometryReader{_ in
ZStack{
if selectedTab == "Post"{
Post(loadedPost: $loadedPost, selectedTab: $selectedTab)
}else if selectedTab == "camera"{
CameraView()
}else if selectedTab == "user"{
User(selectedTab: $selectedTab, loadedPost: $loadedPost)
}
}
}.onChange(of: selectedTab) { (_) in
switch(selectedTab){
case "Post": if
!modelData.isPostLoad{modelData.loadPost()}
case "camera": if
!modelData.isCameraLoad{modelData.loadCamera()}
case "user": if
!modelData.isUserLoad{modelData.loadUser()}
default: ()
}
}
//Tabview hide to show friend modal
if !showSheet{
Divider()
HStack(spacing: 0) {
Spacer(minLength: 0)
TabButton(title: "Post", selectedTab: $selectedTab)
Spacer(minLength: 0)
TabButton(title: "camera", selectedTab: $selectedTab)
.padding(.leading, 30)
.padding(.trailing, 30)
Spacer(minLength: 0)
TabButton(title: "user", selectedTab: $selectedTab)
Spacer(minLength: 0)
}
.padding(.horizontal, 30)
.padding(.bottom, edges!.bottom == 0 ? 15 : edges!.bottom)
.background(Color.black)
}
}
.ignoresSafeArea(.all, edges: .bottom)
.background(Color("Black").ignoresSafeArea(.all, edges: .all))
}
}
}
//Tab Button
struct TabButton : View {
var title: String
#Binding var selectedTab: String
var body: some View {
Button(action: {
withAnimation{selectedTab = title}
}) {
VStack(spacing: 5) {
//Top indicator
//Custom shape...
if title == "user" {
Image(title)
.renderingMode(.template)
.resizable()
.foregroundColor(selectedTab == title ? Color.white : Color("Grey"))
.frame(width: 26.5, height: 26.5)
.padding(.top, UIScreen.screenHeight < 500 ? -5 : 15)
}else if title == "camera"{
Image(title)
.renderingMode(.template)
.resizable()
.foregroundColor(selectedTab == title ? Color.white : Color("Grey"))
.frame(width: 40, height: 40)
.padding(.top, UIScreen.screenHeight < 500 ? -5 : 15)
}else{
Image(title)
.renderingMode(.template)
.resizable()
.foregroundColor(selectedTab == title ? Color.white : Color("Grey"))
.frame(width: 32.5, height: 32.5)
.padding(.top, UIScreen.screenHeight < 500 ? -5 : 15)
}
}
}
}
}
//can update with load views here
class ModelView: ObservableObject {
#Published var isPostLoad = false
#Published var isCameraLoad = false
#Published var isUserLoad = false
init() {
//load initial data
isCameraLoad = true
print("Home Data Loaded")
}
func loadPost(){
print("Post Loaded")
isPostLoad = true
}
func loadCamera(){
print("Camera Loaded")
isCameraLoad = true
}
func loadUser(){
print("User loaded")
isUserLoad = true
}
}
I would greatly appreciate any help on how to get the ImagePicker view to show up for iOS 14.1-ios 14.4 I've been scratching my head since I worked on it assuming anything that works on iOS 14.5 and above should work on below but only this specific ImagePicker is not working as intended. Thanks!

Removing items from a child view generated by For Each loop causes Fatal Error

I hope you are having a more pleasant evening than mine!
So as I mentioned in the title, my for each loop crashes whenever I try to remove an item from the original list with a binding. I did some research and the problem is that for each generates a view with an id but when you delete the item in your child view it can't find the contents and crashes. Returns 'Thread 1: Fatal error: Index out of range'. I can fix the issue by declaring a #State var instead of #Binding, which really works! However, I have more than a delete button in my child view and if I don't use a binding declaration, changes made don't reflect on the main view. I don't wanna give up on neither the delete button nor buttons. Is there way to keep all of them in my childview?
Mainview declarations;
struct ContentView: View {
#ObservedObject var superReminders = SuperReminders()
#State var superReminder = SuperReminder()}
My list View;
List{
ForEach(superReminders.reminderlist.indices, id: \.self) { index in
NavigationLink(destination: DetailedRemView(superReminder : self.$superReminders.reminderlist[index] ).environmentObject(superReminders)) {
squareImageView(superReminder : self.$superReminders.reminderlist[index]).environmentObject(superReminders).environmentObject(superReminders)
}.listRowBackground(Color.clear)
}.onDelete { indexSet in
superReminders.reminderlist.remove(atOffsets: indexSet)}
}
Childview declarations;
import SwiftUI
struct DetailedRemView: View {
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "EE, MMM d, YYYY"
return formatter
}
#State public var showingDetail = false
#Environment(\.colorScheme) var colorScheme: ColorScheme
#State private var deleteReminderAlert = false
#EnvironmentObject var superReminders : SuperReminders
#Environment(\.presentationMode) var presentationMode
#Binding var superReminder : SuperReminder
#State private var showDialog = false
#State var animate = false
var body: some View {
VStack{
HStack(alignment: .center){
Text(superReminder.remdate)
.font(.title)
.multilineTextAlignment(.leading)
.padding(.leading)
.frame(minWidth: 100,maxWidth: .infinity, maxHeight: 50)
Spacer()
Button(action: {
self.showDialog.toggle()
}, label: {
ZStack{
RoundedRectangle(cornerRadius: 10)
.fill(Color.blue)
.frame(width: 80, height: 35)
HStack{
Text("Edit")
.foregroundColor(.white)
.multilineTextAlignment(.center)
.cornerRadius(8)
Image(systemName: "pencil")
.foregroundColor(.white)
}
}
.shadow(color:Color.gray.opacity(0.3), radius: 3, x: 3, y: 3)
.padding(.leading)
.alert(isPresented: $showDialog,
TextAlert(title: "Edit reminder title",
message: "Enter a new title or dissmis.", placeholder: superReminder.remdate,
keyboardType: .default) { result in
if let text = result {
if text != "" {
superReminder.remdate = text }
else{}
} else {
}
})
})
.padding(.leading)
}
.frame(minWidth: 100, maxWidth: /*#START_MENU_TOKEN#*/.infinity/*#END_MENU_TOKEN#*/, maxHeight: 50, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.padding(.vertical, -10)
ZStack(alignment: .topTrailing){
Image(superReminder.image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0,maxWidth: .infinity,minHeight: 200,maxHeight: .infinity)
.saturation(superReminder.pastreminder ? 0.1 : 1)
.clipShape(Rectangle())
.cornerRadius(10)
.padding(.all)
.pinchToZoom()
HStack{
Text(superReminder.dateactual, formatter: dateFormatter)
.foregroundColor(.white)
.frame(width: 180, height: 30, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.background( superReminder.pastreminder ? Color.gray : Color.lightlygreen)
.cornerRadius(8)
.animation(/*#START_MENU_TOKEN#*/.easeIn/*#END_MENU_TOKEN#*/)
if superReminder.pastreminder == true {
ZStack{
RoundedRectangle(cornerRadius: 8)
.fill(Color.black)
.frame(width: 30, height: 30)
Image(systemName: "moon.zzz")
.foregroundColor(.white)
}.offset(x: animate ? -3 : 0)
.onAppear(perform: {
shake()
})
}
else{}
}
.zIndex(0)
.offset(x: -10, y: 10)
.padding()
}
.zIndex(1)
.shadow(color: Color.gray.opacity(0.4), radius: 3, x: 1, y: 2)
HStack{
Button(action: {
self.showingDetail.toggle()
}){
ZStack{
RoundedRectangle(cornerRadius: 10)
.fill(Color.lightlygreen)
.frame(width: 140, height: 40)
HStack{
Text("Reschedule")
.foregroundColor(.white)
.multilineTextAlignment(.center)
.cornerRadius(8)
Image(systemName: "calendar")
.foregroundColor(.white)
}
}
.shadow(color:Color.gray.opacity(0.3), radius: 3, x: 3, y: 3)
.padding(.all, 4.0)
}
.sheet(isPresented: $showingDetail, content :{
remdatepicker(isPresented: self.$showingDetail, superReminder: $superReminder)})
Button(action: {
if superReminder.pastreminder == true {
superReminder.pastreminder = false
}
else if superReminder.pastreminder == false{
superReminder.pastreminder = true
}
}, label: {
ZStack{
RoundedRectangle(cornerRadius: 10)
.fill(superReminder.pastreminder == true ? Color.lightlyblue : Color.gray)
.frame(width: 100, height: 40)
HStack{
Text(superReminder.pastreminder == true ? "Activate" : "Silence")
.foregroundColor(.white)
.multilineTextAlignment(.center)
.cornerRadius(8)
Image(systemName: superReminder.pastreminder == true ? "checkmark.circle" : "moon.zzz")
.foregroundColor(.white)
}
}
.shadow(color:Color.gray.opacity(0.3), radius: 3, x: 3, y: 3)
.padding(.all, 4.0)
})
Button(action: {
self.deleteReminderAlert.toggle()
}, label: {
ZStack{
RoundedRectangle(cornerRadius: 10)
.fill(Color(.red))
.frame(width: 40, height: 40)
HStack{
Image(systemName: "trash")
.foregroundColor(.white)
}
}
.shadow(color:Color.gray.opacity(0.3), radius: 3, x: 3, y: 3)
.padding(.all, 4.0)
})
}.padding(.bottom, 20)
.alert(isPresented: $deleteReminderAlert){
Alert(
title: Text("Are you sure?"),
message: Text("Do you want to delete this reminder?"),
primaryButton: .destructive(Text("Yes"), action: {
superReminders.remove(superReminder: superReminder)
self.presentationMode.wrappedValue.dismiss()
}),
secondaryButton: .cancel(Text("No"))
)
}
}
}
func shake() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
withAnimation(Animation.default.repeatCount(6).speed(7)){
animate.toggle()}}}
}
Class and List;
import SwiftUI
struct SuperReminder: Identifiable, Codable, Equatable {
var id = UUID()
var remdate = ""
var dateactual = Date.init()
var image = "New1"
var pastreminder = false
}
class SuperReminders: ObservableObject {
#Published var reminderlist: [SuperReminder]
init() {
self.reminderlist = [
]
}
func add(superReminder: SuperReminder) {
reminderlist.append(superReminder)
}
func remove(superReminder: SuperReminder) {
if let index = reminderlist.firstIndex(of: superReminder) {
reminderlist.remove(at: index)
}
}
}
This answer is similar to
Accessing and manipulating array item in an EnvironmentObject
Loop over superReminders.reminderlist since SuperReminder: Identifiable, Codable, Equatable.
ForEach(superReminders.reminderlist) { superReminder in
NavigationLink(destination: DetailedRemView(superReminders: superReminders,
superReminder: superReminder)) {
-----
}
}
In DetailedRemView, do the following:
struct DetailedRemView: View {
#ObservedObject var superReminders : SuperReminders
var superReminder : SuperReminder
// find index of current superReminder
var indexOfReminder: Int? {
superReminders.reminderlist.firstIndex {$0 == superReminder}
}
var body: some View {
// Unwrap indexOfReminder
if let index = indexOfReminder {
VStack {
------
}
}
}
----
}
Use superReminders.reminderlist[index] in DetailRemView whereever you need to update superReminder.
superReminders.reminderlist[index].pastreminder = false

SwiftUI custom modal view transition lagging

I have a custom modal View which is part of the ZStack which overlays the other content when enabled.
When a button is pressed, I want the modal sheet to transition from the bottom edge of the device to the centre of the screen, which I have somewhat accomplished. However, the animation somewhat fails when dismissing the modal view, as seen in the provided video, and I'm having difficulties figuring out why this is.
The animation of the modal view I'm using is
.animation(Animation.spring().speed(1.5))
.transition(.move(edge: .bottom))
For the sake of completion, here is my modal view:
struct AddEventView: View {
#State var eventName: String = ""
#State var endDate = Date().addingTimeInterval(60)
#State var gradientIndex: Int = 0
#EnvironmentObject var model: Model
let existingEvent: Event?
let linearGradients: [LinearGradient] = Gradient.gradients.map {
LinearGradient(
gradient: $0,
startPoint: .topTrailing,
endPoint: .bottomLeading
)
}
/// This closure is invoked when the view is dimissed, with a newly created Event passed as its parameter.
/// If the user cancelled this action, `nil` is passed as the parameter
let onDismiss: (Event?) -> Void
var body: some View {
print("Redrawing AddEventView")
return VStack(spacing: 30.0) {
HStack {
Spacer().frame(width: 44)
Spacer()
Text(existingEvent == nil ? "Create Event" : "Edit Event")
.font(.title3)
.bold()
Spacer()
Button(action: {
onDismiss(nil)
}) {
Image(systemName: "xmark.circle.fill")
.imageScale(.large)
}
.frame(width: 44)
}
.padding(.bottom, 5)
.padding(.top, 8)
HStack {
Text("Name of Event").padding(.trailing, 20)
TextField("My Birthday", text: $eventName)
.frame(height: 35)
}
DatePicker(
"Date of Event".padding(toLength: 19, withPad: " ", startingAt: 0),
selection: $endDate,
in: Date()...
)
.frame(height: 35)
ColorChooser(
linearGradients,
selectedIndex: $gradientIndex
)
.frame(height: 75)
Button(action: {
let adjustedEnd = Calendar.current.date(bySetting: .second, value: 0, of: endDate)
let event = Event(
name: eventName,
start: existingEvent?.start ?? Date(),
end: adjustedEnd!,
gradientIndex: gradientIndex
)
onDismiss(event)
}) {
RoundedRectangle(cornerRadius: 13)
.frame(maxWidth: .infinity)
.frame(height: 42)
.overlay(
Text(existingEvent == nil ? "Add Event" : "Edit Event")
.foregroundColor(.white)
.bold()
)
.padding(.horizontal, 1)
}
.padding(.top, 8)
.disabled(self.eventName.isEmpty)
}
.padding(.all, 16)
.background(Color.white)
.cornerRadius(16)
.shadow(radius: 16)
.onAppear {
if let event = existingEvent {
self.eventName = event.name
self.endDate = event.end
self.gradientIndex = event.gradientIndex
}
}
}
}
and my ContentView:
struct ContentView: View {
#State var progress: Double = 0.0
#State var showModal: Bool = false
#State var showPopover: Bool = false
#State var modifiableEvent: Event?
#State var now: Date = Date()
#State var confettiView = ConfettiUIView()
#EnvironmentObject var model: Model
let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect()
let columns: [GridItem] = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2)
var alertButtons: [Alert.Button] {
return Model.SortableKeyPaths.map { key, _ in
.default(Text(key)) { model.sortedKey = key }
}
}
func onEventEnd() {
self.confettiView.emit(with: [.text("๐ŸŽ‰")])
AudioManager.shared.play("Success 1.mp4")
let taptics = UINotificationFeedbackGenerator()
taptics.notificationOccurred(.success)
}
var grid: some View {
LazyVGrid(columns: columns, spacing: 10) {
ForEach(model.events, id: \.self) { event in
SmallCardView(event: event)
.contextMenu {
Button(action: {
modifiableEvent = event
withAnimation {
self.showModal = true
}
}) {
Text("Edit")
Image(systemName: "slider.horizontal.3")
}
Button(action: {
model.removeEvent(event)
}) {
Text("Delete")
Image(systemName: "trash")
}
}
.animation(.linear)
}
if !showModal || modifiableEvent != nil {
AddEventButtonView() {
modifiableEvent = nil
self.showModal = true
}
} else {
Spacer().frame(height: 100)
}
}
.navigationBarTitle(Text("My Events"), displayMode: .large)
.navigationBarItems(
leading: Button(action: { }) {
Image(systemName: "ellipsis")
.imageScale(.large)
},
trailing: Button(action: { self.showPopover = true }) {
Image(systemName: "arrow.up.arrow.down").imageScale(.large)
}
.actionSheet(isPresented: $showPopover) {
ActionSheet(
title: Text("Sort Events"),
buttons: alertButtons + [.cancel()]
)
}
)
}
var body: some View {
return ZStack {
NavigationView {
ScrollView {
grid.padding(.horizontal, 16)
}
.padding(.top)
}
.brightness(self.showModal ? -0.1 : 0)
.blur(radius: self.showModal ? 16 : 0)
.scaleEffect(self.showModal ? 0.95 : 1)
if self.showModal {
AddEventView(existingEvent: modifiableEvent) { event in
if let event = event {
self.model.removeEvent(modifiableEvent)
self.model.addEvent(event)
}
withAnimation {
self.showModal = false
}
}
.padding(.horizontal, 16)
.zIndex(1.0)
.animation(Animation.spring().speed(1.5))
.transition(.move(edge: .bottom))
}
EmptyView().id("\(self.now.hashValue)")
}
.overlay(
UIViewWrapper(view: $confettiView)
.edgesIgnoringSafeArea(.all)
.allowsHitTesting(false)
)
.onReceive(timer) { _ in
if !showModal { self.now = Date() }
if model.events.contains(where: { -1...0 ~= $0.timeRemaining }) {
onEventEnd()
}
}
}
}

Resources