-
Notifications
You must be signed in to change notification settings - Fork 5
Add image gallery to articles #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
5579173
3e138c8
db617f6
962ce25
308616a
5af018c
48a0dfd
91a239f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,19 @@ | ||
| { | ||
| "sourceLanguage" : "en", | ||
| "strings" : { | ||
| "" : { | ||
|
|
||
| }, | ||
| "%lld / %lld" : { | ||
|
||
| "localizations" : { | ||
| "en" : { | ||
| "stringUnit" : { | ||
| "state" : "new", | ||
| "value" : "%1$lld / %2$lld" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "%lld people voted" : { | ||
| "localizations" : { | ||
| "ru" : { | ||
|
|
@@ -163,6 +176,26 @@ | |
| } | ||
| } | ||
| }, | ||
| "Save" : { | ||
| "localizations" : { | ||
| "ru" : { | ||
| "stringUnit" : { | ||
| "state" : "translated", | ||
| "value" : "Сохранить" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "Share" : { | ||
| "localizations" : { | ||
| "ru" : { | ||
| "stringUnit" : { | ||
| "state" : "translated", | ||
| "value" : "Поделиться" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "Share Link" : { | ||
| "localizations" : { | ||
| "ru" : { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,9 @@ struct ArticleElementView: View { | |
| @State private var gallerySelection: Int = 0 | ||
| @State private var pollSelection: ArticlePoll.Option? | ||
| @State private var pollSelections: Set<ArticlePoll.Option> = .init() | ||
| @State private var showFullScreenImage: Bool = false | ||
| @State private var showFullScreenGallery: Bool = false | ||
|
||
| @State private var selectedImageID: Int = 0 | ||
|
|
||
| private var hasSelection: Bool { | ||
| return pollSelection != nil || !pollSelections.isEmpty | ||
|
|
@@ -113,14 +116,20 @@ struct ArticleElementView: View { | |
| .frame(width: UIScreen.main.bounds.width, | ||
| height: UIScreen.main.bounds.width * element.ratioHW) | ||
| .clipped() | ||
| .onTapGesture { | ||
| showFullScreenImage.toggle() | ||
| } | ||
| .fullScreenCover(isPresented: $showFullScreenImage) { | ||
| TabViewGallery(gallery: [element.url], showScreenGallery: $showFullScreenImage, selectedImageID: $selectedImageID) | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Gallery | ||
|
|
||
| @ViewBuilder | ||
| private func gallery(_ element: [ImageElement]) -> some View { | ||
| TabView { | ||
| ForEach(element, id: \.self) { imageElement in | ||
| ForEach(Array(element.enumerated()), id: \.element) { index, imageElement in | ||
| LazyImage(url: imageElement.url) { state in | ||
| Group { | ||
| if let image = state.image { | ||
|
|
@@ -133,13 +142,22 @@ struct ArticleElementView: View { | |
| } | ||
| .aspectRatio(imageElement.ratioWH, contentMode: .fit) | ||
| .clipped() | ||
| .highPriorityGesture( | ||
| TapGesture().onEnded { | ||
| showFullScreenGallery.toggle() | ||
| selectedImageID = index | ||
| } | ||
| ) | ||
| } | ||
| .padding(.bottom, 48) // Fix against index overlaying | ||
| } | ||
| .frame(height: CGFloat(element.max(by: { $0.ratioHW < $1.ratioHW})!.ratioHW) * UIScreen.main.bounds.width + 48) | ||
| .tabViewStyle(.page(indexDisplayMode: .always)) | ||
| .indexViewStyle(.page(backgroundDisplayMode: .always)) | ||
| .padding(.bottom, -16) | ||
| .fullScreenCover(isPresented: $showFullScreenGallery) { | ||
| TabViewGallery(gallery: element.map{ $0.url }, showScreenGallery: $showFullScreenGallery, selectedImageID: $selectedImageID) | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Video | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| // | ||
| // CustomScrollView.swift | ||
| // ArticleFeature | ||
| // | ||
| // Created by Виталий Канин on 11.03.2025. | ||
| // | ||
|
|
||
| import SwiftUI | ||
| import UIKit | ||
|
||
| import Models | ||
|
|
||
| struct CustomScrollView: UIViewRepresentable { | ||
|
|
||
| let imageElement: [URL] | ||
| @Binding var selectedIndex: Int | ||
| @Binding var isZooming: Bool | ||
| @Binding var isTouched: Bool | ||
| @Binding var backgroundOpacity: Double | ||
| var onClose: (() -> Void)? | ||
|
|
||
| func makeUIView(context: Context) -> UICollectionView { | ||
|
|
||
| let layout = UICollectionViewFlowLayout() | ||
| layout.scrollDirection = .horizontal | ||
| layout.minimumLineSpacing = 0 | ||
| layout.itemSize = UIScreen.main.bounds.size | ||
|
|
||
| let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) | ||
| collectionView.isPagingEnabled = true | ||
| collectionView.showsHorizontalScrollIndicator = false | ||
| collectionView.showsVerticalScrollIndicator = false | ||
| collectionView.backgroundColor = .clear | ||
| collectionView.dataSource = context.coordinator | ||
| collectionView.delegate = context.coordinator | ||
| collectionView.backgroundColor = .black | ||
| collectionView.register(ImageCollectionViewCell.self, forCellWithReuseIdentifier: "ImageCollectionViewCell") | ||
|
|
||
| let panGesture = UIPanGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleVerticalSwipe(_:))) | ||
| panGesture.delegate = context.coordinator as any UIGestureRecognizerDelegate | ||
| collectionView.addGestureRecognizer(panGesture) | ||
|
|
||
| return collectionView | ||
| } | ||
|
|
||
| func updateUIView(_ uiView: UICollectionView, context: Context) { | ||
| let indexPath = IndexPath(item: selectedIndex, section: 0) | ||
| DispatchQueue.main.async { | ||
|
||
| uiView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false) | ||
| } | ||
| } | ||
|
|
||
| func makeCoordinator() -> Coordinator { | ||
| Coordinator(self) | ||
| } | ||
|
|
||
| class Coordinator: NSObject, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UIGestureRecognizerDelegate{ | ||
| var parent: CustomScrollView | ||
| private var initialTouchPoint: CGPoint = .zero | ||
| private var firstSwipeDirection: SwipeDirection = .none | ||
|
|
||
| enum SwipeDirection { | ||
| case horizontal | ||
| case vertical | ||
| case none | ||
| } | ||
|
|
||
| init(_ parent: CustomScrollView) { | ||
| self.parent = parent | ||
| } | ||
|
|
||
| func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { | ||
| return parent.imageElement.count | ||
| } | ||
|
|
||
| func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { | ||
| let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCollectionViewCell", for: indexPath) as! ImageCollectionViewCell | ||
| cell.setImage(url: parent.imageElement[indexPath.item]) | ||
|
|
||
| cell.onZoom = { isZooming in | ||
| DispatchQueue.main.async { | ||
|
||
| self.parent.isZooming = isZooming | ||
| } | ||
| } | ||
|
|
||
| cell.onToolBar = { | ||
| DispatchQueue.main.async { | ||
|
||
| self.parent.isTouched.toggle() | ||
| } | ||
| } | ||
| return cell | ||
| } | ||
|
|
||
| func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { | ||
| let pageIndex = Int(scrollView.contentOffset.x / scrollView.bounds.width) | ||
| self.parent.selectedIndex = pageIndex | ||
| scrollView.gestureRecognizers!.last!.isEnabled = true | ||
|
||
| firstSwipeDirection = .none | ||
| } | ||
|
|
||
| @objc func handleVerticalSwipe(_ gesture: UIPanGestureRecognizer) { | ||
| guard let collectionView = gesture.view as? UICollectionView else { return } | ||
| guard let visibleCell = collectionView.visibleCells.first as? ImageCollectionViewCell else { return } | ||
| let translation = gesture.translation(in: gesture.view?.superview) | ||
| if parent.isZooming { return } | ||
|
|
||
| switch gesture.state { | ||
| case .began: | ||
| initialTouchPoint = gesture.location(in: gesture.view?.superview) | ||
| case .changed: | ||
| if abs(translation.y) > abs(translation.x) && firstSwipeDirection == .vertical { | ||
| collectionView.isScrollEnabled = false | ||
| visibleCell.transform = CGAffineTransform(translationX: 0, y: translation.y) | ||
| self.parent.backgroundOpacity = max(0.1, 1 - Double(abs(translation.y * 5) / 700)) | ||
| collectionView.layer.opacity = max(0.1, 1 - Float(abs(translation.y * 2.5) / 700)) | ||
| } | ||
| case .ended, .cancelled: | ||
| if abs(translation.y) > 150 { | ||
| parent.onClose?() | ||
| UIView.animate(withDuration: 0.6, | ||
| delay: 0, | ||
| usingSpringWithDamping: 0.8, | ||
| initialSpringVelocity: 0.3, | ||
| options: .curveEaseInOut, | ||
| animations: { | ||
| self.parent.backgroundOpacity = 0.0 | ||
| collectionView.layer.opacity = 0.0 | ||
| }) | ||
| } else { | ||
| UIView.animate(withDuration: 0.6, | ||
| delay: 0, | ||
| usingSpringWithDamping: 0.8, | ||
| initialSpringVelocity: 0.3, | ||
| options: .curveEaseInOut, | ||
| animations: { | ||
| visibleCell.transform = CGAffineTransform(translationX: 0, y: 0) | ||
| self.parent.backgroundOpacity = 1.0 | ||
| collectionView.layer.opacity = 1.0 | ||
| }) | ||
| } | ||
| firstSwipeDirection = .none | ||
| collectionView.isScrollEnabled = true | ||
| case .failed: | ||
| print("failed") | ||
|
||
| case .possible: | ||
| print("possible") | ||
| @unknown default: | ||
| break | ||
| } | ||
| } | ||
|
|
||
| func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { | ||
| guard let collection = otherGestureRecognizer.view as? UICollectionView else { | ||
| return false | ||
| } | ||
|
|
||
| if parent.imageElement.count == 1 { | ||
| firstSwipeDirection = .vertical | ||
| return true | ||
| } | ||
|
|
||
| if let panGesture = gestureRecognizer as? UIPanGestureRecognizer { | ||
| let velocity = panGesture.velocity(in: panGesture.view) | ||
| if firstSwipeDirection == .none { | ||
| if abs(velocity.x) > abs(velocity.y) { | ||
| firstSwipeDirection = .horizontal | ||
| collection.isScrollEnabled = true | ||
| panGesture.isEnabled = false // | ||
| } else if abs(velocity.x) < abs(velocity.y) { | ||
| firstSwipeDirection = .vertical | ||
| otherGestureRecognizer.isEnabled = true | ||
| collection.isScrollEnabled = false | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return true | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Пустая локализация, скорее всего надо просто завернуть вызов в String