SwiftUI custom animation does not apply on inner view change - ios

I have a custom bounce animation applied to a view with multiple inner views which can change depending on conditions. My issue is that when the inner view changes, the animation applied to a parent no longer applies to it.
Example:
Sample Code:
struct ContentView: View {
#State var number = 1
var body: some View {
VStack {
ZStack {
Circle()
.foregroundColor(.gray)
.frame(width: 100, height: 100, alignment: .center)
if number == 1 {
Image(systemName: "person")
}
else if number == 2 {
ProgressView()
}
else {
EmptyView()
}
}
.bounceEffect()
Button {
if number != 3 {
number += 1
}
else {
number = 1
}
} label: {
Text("CHANGE")
}
.padding()
}
.padding()
}
}
struct BounceEffect: ViewModifier {
#State var bounce = false
var allow: Bool
func body(content: Content) -> some View {
content
.offset(y: (bounce && allow) ? -5 : 0)
.animation(.interpolatingSpring(mass: 1, stiffness: 350, damping: 5, initialVelocity: 10).repeatForever(autoreverses: false).delay(1), value: UUID())
.onAppear {
if allow {
bounce.toggle()
}
}
}
}
extension View {
func bounceEffect(allow: Bool = true) -> some View {
modifier(BounceEffect(allow: allow))
}
}
Thank you.

The reason of the bounceEffect don't get the button after you change because you are creating the new image when ever you tap the button change.
The action you need here: just only change the system image of the image not creating new one every time the button change is tapped
The code will be like this
struct ContentView: View {
#State var number = 1
#State var imageName = "person"
var body: some View {
VStack {
ZStack {
Circle()
.foregroundColor(.gray)
.frame(width: 100, height: 100, alignment: .center)
Image(systemName: imageName)
}
.bounceEffect()
Button {
if number != 3 {
number += 1
}
else {
number = 1
}
if number == 1 {
imageName = "person"
}
else if number == 2 {
imageName = "globe"
}
else {
imageName = "square"
}
} label: {
Text("CHANGE")
}
.padding()
}
.padding()
}
}
The result

When the new images appear, they aren’t the ones involved in the animation. One way to fix this is to include all 3 images from the start, and just cycle their visibility by changing opacity:
ZStack {
Circle()
.foregroundColor(.gray)
.frame(width: 100, height: 100, alignment: .center)
Image(systemName: "person")
.opacity(number == 1 ? 1 : 0)
Image(systemName: "globe")
.opacity(number == 2 ? 1 : 0)
Image(systemName: "square")
.opacity(number == 3 ? 1 : 0)
}
#bewithyou’s answer is more scalable and should be the accepted answer. Here is a better way to structure the selection of your images:
struct ContentView: View {
#State var number = 0
let names = ["person", "globe", "square", "house", "car"]
var body: some View {
VStack {
ZStack {
Circle()
.foregroundColor(.gray)
.frame(width: 100, height: 100, alignment: .center)
Image(systemName: names[number])
}
.bounceEffect()
Button {
number = (number + 1) % names.count
} label: {
Text("CHANGE")
}
.padding()
}
.padding()
}
}

Related

How do I change and persist the button color after favoriting and then switch back in SwiftUI?

I'm running into an issue I can't figure out. How do you save the color of a button once a user clicks on it and have it switch back when the uncheck it? I have a Favorites button that when a user clicks on it, the color, SF symbol, and text changes from "Favorite" to "Favorited". That works fine. However, when I leave the screen, everything goes back to the original colors.
Here is my code.
import SwiftUI
struct CharacterDetailsView: View {
//Need to reference for detail view by relying on the list to assign the details.
var character: Character
#EnvironmentObject var favorites: Favorites
#State private var isFavorited = false
var favoriteText: String {
if isFavorited {
return "Favorited"
} else {
return "Favorite"
}
}
var favoriteButton: some View {
Button {
isFavorited.toggle()
if favorites.contains(character) {
//If favorites contains a character, we are trying to un-favorite and remove it
favorites.remove(character)
} else {
favorites.add(character)
}
} label: {
ZStack {
if isFavorited == false {
Capsule()
.strokeBorder(Color.green, lineWidth: 4)
.frame(width: 250, height: 50)
.background(
Capsule()
.fill(.green)
.cornerRadius(20)
)
} else {
Capsule()
.strokeBorder(Color.blue, lineWidth: 4)
.frame(width: 250, height: 50)
.background(
Capsule()
.fill(.blue)
.cornerRadius(20)
)
}
HStack {
Text(favoriteText)
.foregroundColor(.white)
Image(systemName: favorites.contains(character) ? "heart.fill" : "heart")
.foregroundColor(favorites.contains(character) ? .white : .white)
}
}
}
.padding(.vertical)
}
var body: some View {
ScrollView {
VStack {
//MARK: - Image
AsyncImage(url: URL(string: character.image)) {
phase in
if let image = phase.image {
image
.resizable()
.scaledToFit()
} else if phase.error != nil {
Text("Couldn't upload photo")
} else {
ProgressView()
}
}
VStack(alignment: .leading) {
Text(character.name)
.font(.largeTitle)
.bold()
.padding(.leading)
Section {
SubDetailView(sfImageString: "checkmark.circle", infoText: "Status", detailText: character.status)
SubDetailView(sfImageString: "person.circle", infoText: "Species", detailText: character.species)
SubDetailView(sfImageString: "house.circle", infoText: "Origin", detailText: character.origin.name)
SubDetailView(sfImageString: "mappin.circle", infoText: "Location", detailText: character.location.name)
}
.padding(.horizontal)
}
favoriteButton
}
}
}
}
You need to restore state from environment object where you stored provious one, like
favoriteButton
.onAppear {
isFavorite = favorites.contains(character) // << here !!
}

SwiftUI - Center Align View With respect to another view which is in HStack

I have a view as show in the below image
Segments will be dynamic, so i have followed loop mechanism
HStack {
for() {
VStack {
HStack {
Image
Line View/. Don't show for last item
}
Text
}
}
}
I have tried with alignmentGuide and resulting the below image
Getting Text, Circles and lines aligned is trickier than I expected. I only made it with some hard-coded values, but at least it is flexible to some extent.
struct Step: Identifiable {
var id: Int
var name: String
var completed: Bool = false
}
struct OrderStateView: View {
var steps: [Step]
var body: some View {
ZStack {
// circles
HStack(spacing: 0) {
Spacer()
ForEach(steps.indices) { index in
let completed = steps[index].completed
Image(systemName: completed ?
"checkmark.circle.fill" : "circle")
.font(.title2)
.foregroundColor(completed ? .green : .gray)
.overlay {
Text(steps[index].name)
.font(.caption)
.multilineTextAlignment(.center)
.offset(x: 0, y: 35)
.frame(minWidth: 70, minHeight: 50)
}
if index < steps.count-1 {
if steps[index+1].completed {
Color.green
.frame(height: 1)
} else {
Color.gray
.frame(height: 1)
}
}
}
}
Spacer()
}
.padding(.bottom, 35)
}
}
// example usage
struct ContentView: View {
#State private var steps = [
Step(id: 0, name: "Customer Details"),
Step(id: 1, name: "Order Type"),
Step(id: 2, name: "Add Products"),
Step(id: 3, name: "Order")
]
#State private var step = 0
var body: some View {
VStack {
Text("Your Order State")
.padding()
OrderStateView(steps: steps)
Button("Continue") {
steps[step].completed = true
if step < steps.count-1 {
step += 1
}
}
.buttonStyle(.bordered)
.padding()
}
.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!

SwiftUI align navigationBarItems buttons with large navigationBarTitle like in "Messages"

I am creating a SwiftUI app and I am using NavigationView and a large title bar. However, I don't like that navigationBarItems buttons are not aligned with the large title bar (3rd picture) like in the Messages app (first and second picture). I tried to reposition the button, but it wasn't clickable anymore. Does anybody have an idea how to solve this? Thanks!
2nd:
3rd:
Solution: (found here: https://www.hackingwithswift.com/forums/swiftui/icons-in-navigationview-title-apple-messages-style/592)
import SwiftUI
struct ContentView: View {
#State private var midY: CGFloat = 0.0
#State private var headerText = "Contacts"
var body: some View {
NavigationView {
List {
HStack {
//text
HeaderView(headerText: self.headerText, midY: $midY)
.frame(height: 40, alignment: .leading)
.padding(.top, 5)
.offset(x: -45)
HStack {
//button 1
Button(action: {
self.action1()
}) {
Image(systemName: "ellipsis.circle")
.font(.largeTitle)
}
//button 2
Button(action: {
self.action2()
}) {
Image(systemName: "pencil.circle")
.font(.largeTitle)
}
}.padding(EdgeInsets(top: 5, leading: 0, bottom: 0, trailing: 16))
.foregroundColor(.blue)
} .frame(height: 40, alignment: .leading)
.opacity(self.midY < 70 ? 0.0 : 1.0)
.frame(alignment: .bottom)
ForEach(0..<100){ count in
Text("Row \(count)")
}
}
.navigationBarTitle(self.midY < 70 ? Text(self.headerText) : Text(""), displayMode: .inline)
.navigationBarItems(trailing: self.midY < 70 ? HStack {
//button 1
Button(action: {
self.action1()
}) {
Image(systemName: "ellipsis.circle")
.frame(width: 20, height: 20)
}
//button 2
Button(action: {
self.action2()
}) {
Image(systemName: "pencil.circle")
.frame(width: 20, height: 20)
}
}
:
HStack {
//button 1
Button(action: {
self.action1()
}) {
Image(systemName: "ellipsis.circle")
.frame(width: 0, height: 0)
}
//button 2
Button(action: {
self.action2()
}) {
Image(systemName: "pencil.circle")
.frame(width: 0, height: 0)
}
}
)
}
}
func action1() {
print("do action 1...")
}
func action2() {
print("do action 2...")
}
}
struct HeaderView: View {
let headerText: String
#Binding var midY: CGFloat
var body: some View {
GeometryReader { geometry -> Text in
let frame = geometry.frame(in: CoordinateSpace.global)
withAnimation(.easeIn(duration: 0.25)) {
DispatchQueue.main.async {
self.midY = frame.midY
}
}
return Text(self.headerText)
.bold()
.font(.largeTitle)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
.navigationBarItems(leading:
Button(action: goBack) { HStack { Image("back") } })
.navigationBarItems(trailing: HStack {
Button(action: { self.share() }) { Image("share") }
Button(action: { self.wishlist() }) { Image("wishlist_detail") }})

SwiftUI unable to click buttons in overlay/popover screen

I have a view that I present at the top of other views like a popover view, inside the view I have a couple of buttons. When I add a single button in the overplayed view and tap the button it works. However if I added multiple buttons and try to tap the buttons they don't work. Instead it clicks to the components bellow the view.
I would like to add multiple buttons and click them on the overlay view, I'm not sure what my mistake is on this code:
Here is my code:
struct MenuContent: View {
var body: some View {
List() {
ForEach(0..<2) { _ in
HStack {
ForEach(0..<4) { _ in
Button(action: {
print("tapped button")
}) {
VStack {
Text("Rev")
Image("trash.fill")
.resizable()
.scaledToFit()
.frame(width: 60, height: 60)
}
}.background(Color.blue)
}
}
}
}
}
}
OverlayView
struct OverlayMenu: View {
let width: CGFloat
#Binding var show: Bool
var body: some View {
return ZStack {
HStack {
MenuContent()
.frame(width: self.width, height: 160)
.cornerRadius(10, antialiased: false)
.offset(x: self.show ? 0 : -self.width, y: 285)
.animation(.spring())
Spacer()
}
.shadow(radius: 20)
}
}
}
ContentView
struct ContentView: View {
#State var show = true
var body: some View {
OverlayMenu(width: 350,
show: $show)
}
}
I think there is some trouble with List rows and tap gestures on them. You can deal with it if you want or you may try VStack instead of List and use Divider to divide "rows" and taps on the buttons will be handle as you expect. I changed your example a little to show how it works, I think you can handle design by yourself then:
struct MenuContent: View {
#State var hits = 0
var body: some View {
VStack {
Text("\(hits)")
Divider().frame(width: 250)
ForEach(0..<2) { _ in
ButtonsLine(hits: self.$hits)
Divider().frame(width: 250)
}
}
}
}
struct ButtonsLine: View {
#Binding var hits: Int
var body: some View {
HStack {
ForEach(0..<4) { value in
Button(action: {
print("tapped button")
self.hits += value + 1
}) {
ButtonDesign()
}
}
}
}
}
struct ButtonDesign: View {
var body: some View {
VStack {
Text("Rev")
.foregroundColor(.black)
Image(systemName: "trash")
.resizable()
.scaledToFit()
.foregroundColor(.red)
.frame(width: 60, height: 60)
}
.shadow(radius: 20)
}
}
and the result is:

Resources