SwiftUI tap gesture selecting wrong item - swiftui

So I'm trying to create a custom image picker something like instagram but way more basic. This is how I created the screen using this.
struct NewPostScreen: View {
#StateObject var manager = SelectNewPostScreenManager()
let columns = [GridItem(.flexible(), spacing: 1), GridItem(.flexible(), spacing: 1), GridItem(.flexible(), spacing: 1)]
var body: some View {
ScrollView {
VStack(spacing: 1) {
Image(uiImage: manager.selectedPhoto?.uiImage ?? UIImage(named: "placeholder-image")!)
.resizable()
.scaledToFit()
.frame(width: 350, height: 350)
.id(1)
LazyVGrid(columns: columns, spacing: 1) {
ForEach(manager.allPhotos) { photo in
Image(uiImage: photo.uiImage)
.resizable()
.scaledToFill()
.frame(maxWidth: UIScreen.main.bounds.width/3, minHeight: UIScreen.main.bounds.width/3, maxHeight: UIScreen.main.bounds.width/3)
.clipped()
.onTapGesture {
manager.selectedPhoto = photo
}
}
}
}
}
}
}
The UI looks good and everything but sometimes when I click an image using the tapGesture it gives me an incorrect selectedPhoto for my manager. Here is how my manager looks and how I fetch the photos from the library.
class SelectNewPostScreenManager: ObservableObject {
#Environment(\.dismiss) var dismiss
#Published var selectedPhoto: Photo?
#Published var allPhotos: [Photo] = []
init() {
fetchPhotos()
}
private func assetsFetchOptions() -> PHFetchOptions {
let fetchOptions = PHFetchOptions()
let sortDescriptor = NSSortDescriptor(key: "creationDate", ascending: false)
fetchOptions.sortDescriptors = [sortDescriptor]
return fetchOptions
}
func fetchPhotos() {
print("Fetching Photos")
let options = assetsFetchOptions()
let allAssets = PHAsset.fetchAssets(with: .image, options: options)
DispatchQueue.global(qos: . background).async {
allAssets.enumerateObjects { asset, count, _ in
let imageManager = PHImageManager.default()
let targetSize = CGSize(width: 250, height: 250)
let options = PHImageRequestOptions()
options.isSynchronous = true
imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { image, info in
guard let image = image else { return }
let photo = Photo(uiImage: image)
DispatchQueue.main.async {
self.allPhotos.append(photo)
}
}
}
}
}
}
This is how my photo object looks like as well.
struct Photo: Identifiable {
let id = UUID()
let uiImage: UIImage
}
I have no clue to why the tap gesture is not selecting the right item. Ive spent a couple of hours trying to figure out to why this is happening. I might just end up using the UIImagePickerController instead lol.
Anyways if someone can copy and paste this code into a new project of Xcode and run it on your actual device instead of the simulator. Let me know if its happening to you as well.
I was running it on an iPhone X.

The problem is that the image gesture are extending beyond your defined frame, I am sure there are many ways to fix this, but I solved it by adding the contentShape modifier
Please replace your image code with the following
Image(uiImage: photo.uiImage)
.resizable()
.scaledToFill()
.frame(width: UIScreen.main.bounds.width/3, height: UIScreen.main.bounds.width/3)
.clipped()
.contentShape(Path(CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width/3, height: UIScreen.main.bounds.width/3)))
.onTapGesture {
manager.selectedPhoto = photo
}
contentShape define the hit area for the gesture

Related

SwiftUI - Custom DatePicker - Show layer or backgorund on top of all or how to do it as DatePicker

I'm trying two write a custom Datepicker so that I can modify the button style. As far as I know, SwiftUI doesn't allow to modify it. My base design based on this answer that I've already improved it.
struct CustomDatePickerView: View {
#State private var showPicker = false
#State var selectedText: String
#State var selectedDateLocal : Date = Date()
var body: some View {
VStack {
Button {
withAnimation {
showPicker.toggle()
}
} label: {
Text(selectedText)
.padding()
.padding(.horizontal)
.foregroundColor(.black)
.background(
RoundedRectangle( cornerRadius:10, style: .continuous).fill(Color.yellow.opacity(0.2))
)
.overlay(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.strokeBorder(Color.black, lineWidth: 1)
)
.id(selectedText)
}
.background(
DatePicker("", selection: $selectedDateLocal, in: closedRange, displayedComponents: [.date,.hourAndMinute])
.datePickerStyle(.graphical)
.frame(width: 400, height: 400)
.clipped()
.background(Color.yellow.opacity(0.1).cornerRadius(10))
.opacity(showPicker ? 1 : 0 )
.offset(x: 0, y: 230)
).onChange(of: selectedDateLocal) { newValue in
let format = DateFormatter()
format.timeStyle = .none
format.dateStyle = .short
print("Name changed to \(format.string(from: newValue))!")
selectedText = format.string(from: newValue)
withAnimation {
showPicker.toggle()
}
}
}
}
}
It was working excellent on test view (sorry image upload failure).
When it is placed on the real application, the background/overlay was behind of other views.
I cannot change the Z-level since the UI a bit complicated.
How can we show the DatePicker displayed on background/overlay on top of everything as the DatePicker does? What is the proper way to do this?

How to use UIActivityViewController to share individual post in a ForEach

I have a forEach Array of articles that opens up a detailview for users to read the articles.I have a button to share the article to other apps using UIActivityViewController, mainly will like to share to Wechat though. My problem is to get UIActivityViewController to share a full article, for now i can share only the image like below with a button located inside my detail view:
Button(action: actionSheet) {
Image(systemName: "arrowshape.turn.up.forward.fill")
.font(.headline)
.padding(8)
.background(Color.white)
.cornerRadius(5)
}
The function that pops up the actionsheet:
func actionSheet() {
let activityController = UIActivityViewController(activityItems: [recomendedViewModel.recommend.image], applicationActivities: nil)
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let window = windowScene?.windows.first
if UIDevice.current.userInterfaceIdiom == .unspecified{
activityController.popoverPresentationController?.sourceView = window
activityController.popoverPresentationController?.sourceRect = CGRect(x: UIScreen.main.bounds.width / 3, y: UIScreen.main.bounds.height / 1.5, width: 400, height: 400)
}
window?.rootViewController!.present(activityController, animated: true)
}
My ObservedObject for my detail view is :
#ObservedObject var recomendedViewModel: RecommendedEventsRowViewModel
init(recommend: RecommendedEvents) {
self.recomendedViewModel = RecommendedEventsRowViewModel(recommend: recommend)
}
My Array of articles:
struct RecommendedEventsMainView: View {
#ObservedObject var recomendedVM = RecommendedEventsViewModel()
var body: some View {
VStack(spacing: 15) {
ForEach(recomendedVM.recommended) { recommended in
NavigationLink(destination: RecommendedEventsDetailView(recommend: recommended)) {
NewUpdatedEventsItemView(recommend: recommended)
}
}
}
}
}
I tried putting the ObservedObject from my Array View ( #ObservedObject var recomendedVM = RecommendedEventsViewModel() ) in my detailview and passing it to the actionSheet function above like thus:
let activityController = UIActivityViewController(activityItems: [recomendedVM.recommended], applicationActivities: nil)
It comes blank with no data. Any help is welcome. Thanks in advance.

SwiftUI: VideoPlayer to display different videos stored locally (back and forward buttons)

I'm still new to SwiftUI and have run into a problem. I need to use back and forward buttons to make the Video Player go to the previous/next video (stored locally). The following code works for one video only, the one declared into the init(), but I can't manage to change the video by clicking the back and forward buttons.
I'm using an array of String called videoNames to pass all the video names from the previous View.
Also, I'm using a custom Video Player for this and I'm gonna include the relevant parts of the code.
This is my View:
struct WorkingOutSessionView: View {
let videoNames: [String]
#State var customPlayer : AVPlayer
#State var isplaying = false
#State var showcontrols = false
init(videoNames: [String]) {
self.videoNames = videoNames
self._customPlayer = State(initialValue: AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: videoNames[0], ofType: "mov")!)))
}
var body: some View {
VStack {
CustomVideoPlayer(player: $customPlayer)
.frame(width: 390, height: 219)
.onTapGesture {
self.showcontrols = true
}
GeometryReader {_ in
// BUTTONS
HStack {
// BACK BUTTON
Button(action: {
// code
}, label: {
Image(systemName: "lessthan")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.foregroundColor((Color(red: 243/255, green: 189/255, blue: 126/255)))
.padding()
})
// FORWARD BUTTON
Button(action: {
// code
}, label: {
Image(systemName: "greaterthan")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.foregroundColor((Color(red: 243/255, green: 189/255, blue: 126/255)))
.padding()
})
}
}
}
.offset(y: 35)
.edgesIgnoringSafeArea(.all)
}
This is my custom Video Player:
struct CustomVideoPlayer : UIViewControllerRepresentable {
#Binding var player: AVPlayer
func makeUIViewController(context: UIViewControllerRepresentableContext<CustomVideoPlayer>) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = player
controller.showsPlaybackControls = false
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<CustomVideoPlayer>) {
}
}
I've researched for solutions but couldn't find anything relevant. I've tried to modify my CustomVideoPlayer and not pass a #Binding variable... As well, the init() gave me a lot of headaches as it comes back to errors every time I change something...
Any solution would help guys. I really appreciate your time.
The first thing that you'll need is something to keep track of your position in the video list. I'm using another #State variable for this.
Whenever that state variable changes, you'll need to update your player. I'm using the onChange modifier near the bottom of the code to do this work.
In your CustomVideoPlayer, you need to use updateUIViewController to make sure the player is up-to-date with the parameter being passed in.
Lastly, there's no need for AVPlayer to be a #Binding, since it's a class that is passed by reference, not a struct that is passed by value.
struct WorkingOutSessionView: View {
let videoNames: [String]
#State private var customPlayer : AVPlayer
#State private var currentItem = 0
#State var isplaying = false
#State var showcontrols = false
init(videoNames: [String]) {
self.videoNames = videoNames
self._customPlayer = State(initialValue: AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: videoNames[0], ofType: "mov")!)))
}
var body: some View {
VStack {
CustomVideoPlayer(player: customPlayer)
.frame(width: 390, height: 219)
.onTapGesture {
self.showcontrols = true
}
.onAppear {
self.customPlayer.play()
}
GeometryReader { _ in
// BUTTONS
HStack {
// BACK BUTTON
Button(action: {
currentItem = min(currentItem, currentItem - 1)
}) {
Image(systemName: "lessthan")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.foregroundColor((Color(red: 243/255, green: 189/255, blue: 126/255)))
.padding()
}
// FORWARD BUTTON
Button(action: {
currentItem = min(videoNames.count - 1, currentItem + 1)
}) {
Image(systemName: "greaterthan")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.foregroundColor((Color(red: 243/255, green: 189/255, blue: 126/255)))
.padding()
}
}
}
}
.offset(y: 35)
.edgesIgnoringSafeArea(.all)
.onChange(of: currentItem) { currentItem in
print("Going to:",currentItem)
self.customPlayer.pause()
self.customPlayer = AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: videoNames[currentItem], ofType: "mov")!))
self.customPlayer.play()
}
}
}
struct CustomVideoPlayer : UIViewControllerRepresentable {
var player: AVPlayer
func makeUIViewController(context: UIViewControllerRepresentableContext<CustomVideoPlayer>) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = player
controller.showsPlaybackControls = false
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<CustomVideoPlayer>) {
uiViewController.player = player
}
}
If this were my own project, I'd probably continue to do some refactoring -- maybe move some things to a view model, etc. Also, I'd probably avoid initializing an AVPlayer in a View's init as I mentioned in my last answer to you on your previous question. It works, but there's definitely a risk you'll end up doing too much heavy lifting if the view hierarchy re-renders.

How to save that my button has already been tapped in SwiftUI

I have the following code that, when tapped it will add the number of likes to a post, it will only allow the user to tap it once, works well but when I reload the app I can like it again, been trying to workout the best way to save that it has been tapped already. I have added the button state as false:
#State var buttonTapped = false
Button(action:
{
self.buttonTapped.toggle() //only allow one tap
let like = Int.init(post.likes)!
ref.collection("Posts").document(post.id).updateData(["likes": "\(like
+ 1)"]) { (err) in
if err != nil{
print((err!.localizedDescription))
return
}
// postData.getAllPosts()
print("updated...")
}
}
) {
Image(systemName: "flame")
.resizable()
.frame(width: 20, height: 20)
}.disabled(buttonTapped)
Any pointers in the right direction would be greatly appreciated
You can use UserDefaults to store the value
struct ContentView : View {
#State var buttonTapped : Bool = UserDefaults.standard.bool(forKey: "buttonTapped")
var body : some View {
Button(action: {
UserDefaults.standard.set(true, forKey: "buttonTapped")
buttonTapped.toggle()
}) {
Image(systemName: "flame")
.resizable()
.frame(width: 20, height: 20)
}.disabled(buttonTapped)
}
}

How to disable hit test on views that are outside the frame due to offset in SwiftUI?

I am using a Paged Stack that scrolls horizontally and snaps to the selected index. The functionality works fine unless I have it alongside another view. When I scroll, the offset is blocking the view.
I have omitted .clipped() just so it can be visualized. If I add it, it looks visually fine but it is still interact-able, blocking gestures intended for the view below it.
struct PagedStack<Content: View>: View {
let pageCount: Int
let spacing: CGFloat
#Binding var currentIndex: Int
let content: Content
#GestureState private var translation: CGFloat = 0
init(pageCount: Int, spacing: CGFloat, currentIndex: Binding<Int>, #ViewBuilder content: () -> Content) {
self.pageCount = pageCount
self.spacing = spacing
self._currentIndex = currentIndex
self.content = content()
}
var body: some View {
GeometryReader { geometry in
HStack(spacing: self.spacing) {
self.content.frame(width: geometry.size.width)
}
.frame(width: geometry.size.width, alignment: .leading)
.offset(x: -CGFloat(self.currentIndex) * (geometry.size.width + self.spacing))
.offset(x: self.translation)
.animation(.interactiveSpring())
.gesture(
DragGesture().updating(self.$translation) { value, state, _ in
state = value.translation.width
}.onEnded { value in
let offset = value.translation.width / geometry.size.width
let newIndex = (CGFloat(self.currentIndex) - offset).rounded()
self.currentIndex = min(max(Int(newIndex), 0), self.pageCount - 1)
}
)
}
}
}
Here is the visual:
I fixed it. I added .contentShape(Rectangle()) to make the hit test area limited to the frame.