Posts

Post not yet marked as solved
1 Replies
102 Views
Preface Upon rotating the interface, the UICollectionViewCells overlap, generating an unpleasant animation that for sure can't be used in production. The code The code was executed on iPhone 6S (NN0W2TU/A A1688) with iOS 15.8.2. I could reproduce the issue on iPhone 15 Pro with iOS 17 on simulator as well. SelfConfiguringCell.swift: import UIKit protocol SelfConfiguringCell: UICollectionViewCell { static var reuseIdentifier: String { get } func configure(with image: String) } ISVImageScrollView.swift: Code here CarouselCell.swift: import UIKit import SnapKit class CarouselCell: UICollectionViewCell, SelfConfiguringCell, UIScrollViewDelegate { static var reuseIdentifier: String = "carousel.cell" internal var image: String = "placeholder" { didSet { self.imageView = UIImageView(image: UIImage(named: image)) self.scrollView.imageView = self.imageView } } let scrollView: ISVImageScrollView = { let scrollView = ISVImageScrollView() scrollView.minimumZoomScale = 1.0 scrollView.maximumZoomScale = 30.0 scrollView.zoomScale = 1.0 scrollView.contentOffset = .zero scrollView.bouncesZoom = true return scrollView }() var imageView: UIImageView = { let image = UIImage(named: "placeholder")! let imageView = UIImageView(image: image) return imageView }() func setImage(_ image: String) { self.image = image } func configure(with image: String) { self.setImage(image) self.scrollView.snp.makeConstraints { make in make.left.top.right.bottom.equalTo(contentView) } } override init(frame: CGRect) { super.init(frame: frame) contentView.backgroundColor = UIColor.black scrollView.delegate = self scrollView.imageView = self.imageView contentView.addSubview(scrollView) } required init?(coder: NSCoder) { fatalError("Cannot init from storyboard") } func viewForZooming(in scrollView: UIScrollView) -> UIView? { return self.imageView } } ViewController: import UIKit class ViewController: UICollectionViewController { var currentPage: IndexPath? = nil let images = ["police", "shutters", "depot", "cakes", "sign"] init() { let compositionalLayout = UICollectionViewCompositionalLayout { sectionIndex, environment in let absoluteW = environment.container.effectiveContentSize.width let absoluteH = environment.container.effectiveContentSize.height // Handle landscape if absoluteW > absoluteH { print("landscape") let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1) ) let item = NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1) ) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) return section } else { // Handle portrait print("portrait") let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(absoluteW * 9.0/16.0) ) let item = NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(absoluteW * 9.0/16.0) ) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) return section } } let config = UICollectionViewCompositionalLayoutConfiguration() config.interSectionSpacing = 0 config.scrollDirection = .horizontal compositionalLayout.configuration = config super.init(collectionViewLayout: compositionalLayout) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() collectionView.delegate = self collectionView.dataSource = self collectionView.isPagingEnabled = true // Register cell for reuse collectionView.register(CarouselCell.self, forCellWithReuseIdentifier: CarouselCell.reuseIdentifier) } override func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 } override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return self.images.count } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let reusableCell = collectionView.dequeueReusableCell(withReuseIdentifier: CarouselCell.reuseIdentifier, for: indexPath) as? CarouselCell else { fatalError() } let index : Int = (indexPath.section * self.images.count) + indexPath.row reusableCell.configure(with: self.images[index]) return reusableCell } } Notes I found a similar unanswered question here. I'm sure something can be done about it because if I switch to SwiftUI with a TabView, that according to SwiftUI Introspect documentation for TabViewWithPageStyleType, is using UICollectionView under the hood, I'm not getting that ugly animation anymore. Though I can't switch to SwiftUI to use TabView because on interface rotation it loses the page index (well known bug, see here), which probably is even trickier to workaround.
Posted Last updated
.
Post not yet marked as solved
2 Replies
269 Views
I need to create a carousel component with the following requirements (sorted by relevance): Objectives Every image is 16:9 aspect ratio and resizes to fit the screen. Needs a zoom and pan functionality, possibly the same way as iOS Photos app. Needs to work both in landscape and portrait mode, with a smooth transition between orientations. When orientation changes, the image needs to be rotated to preserve the center of the image (like Photos app and hyperoslo/Lightbox) The component should only take the minimum necessary space. In most use cases, such component should have other subviews both above and below. Circularity. I would like the carousel to wrap around. What I tried: Using a TabView with .tabViewStyle(PageTabViewStyle()).indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always)) modifiers. This didn't work: rotating the interface caused the view to get stuck between pages (it looks like it's a well known [bug]).(https://stackoverflow.com/questions/72435939/swiftui-tabview-is-not-working-properly-on-orientation-change). Implementing a single page (that is, an image view) using an UIScrollView and an UIViewRepresentable, then collecting them into an HStack. Unfortunately I need to use zoomScale and contentOffset properties of the UIScrollView outside of the UIViewRepresentable itself. The net result was that .init() was invoked for every image in the carousel at every rotation, causing severe stutters and an horrible rotation animation. Implementing the whole carousel using UIKit, and an UICollectionView, whose cells were an instance of UIScrollView. The problem is, the UIScrollView needs to recompute its constraints upon rotation but a cell is an instance of UIView, so it can't respond to rotations via viewWillTransition(to:CGSize, with: any UIViewControllerTransitionCoordinator). In the UICollectionView itself, you can only access the visible cells (one at a time is visible), and cells are cached, so even after rotating, some occasionally are presented on screen with the same appearance as before the rotation (some do, some don't, in the same UICollectionView). Also when rotating, it looks like the UIScrollView of the visible cell is being recreated, making it impossible to preserve the image center (I use this subclass of UIScrollView for this purpose). And the UICollectionView is taking the full window size, not just the bare minimum necessary space. Help: With all of this in mind, what options do I realistically have? If necessary I can raise the minimum iOS version to 16.0, even though I guess it doesn't make any significative difference since SwiftUI introduced MagnifyGesture only from iOS 17.0.
Posted Last updated
.
Post not yet marked as solved
0 Replies
321 Views
I need to support SVG rendering in my application and currently I'm adopting the approach of pasting the code to Quassum/svg-to-swiftui website (see here) and making a View out of it. Even though this is somewhat effective, it is not ideal since I'd like to be able to persist a reference to the svg resource's bundle URL and display it. I will need to update the stroke possibly with high frequency (on pinch gesture) but don't need a fill capability. I checked out SVGView from Exyte and it'd be perfect if only it allowed to stroke paths from the SVG. That seems to be the only one out there that's easy to find at least. Any suggestions (I would like to avoid resorting to UIKit and UIViewRepresentable since in my experience it will lead to animations glitches more often than not)?
Posted Last updated
.
Post not yet marked as solved
0 Replies
198 Views
I'm trying to create a simple carousel with infinite circular scroll (that is, swiping on the last image leads to the first one and the other way around). I came up with the following solution but unfortunately, only when scrolling forward, the animation hitches before showing the correct new image. When scrolling backwards, the animation is fine, which confuses me since the logic for backward and forward swipe is symmetrical and shares most of the code. Here is an MRE: I used 5 1920x1080 images named "PH1", "PH2", ... , "PH5" for the sake of this example. CarouselModel.swift class CarouselModel: ObservableObject { var uuid: UUID = UUID() @Published internal var onScreenBuffer: [BufferedImage] = [] private var offScreenBuffer: [BufferedImage] = [] internal var currentImage: Int = 0 internal var offsetImageSelector: Int = 0 @Published internal var imagesIDs: [String] = [] { didSet { guard imagesIDs.count > 0 else { return } self.makeBuffer(for: self.currentImage, buffer: &self.onScreenBuffer) self.makeBuffer(for: self.currentImage, buffer: &self.offScreenBuffer) } } internal var bufferedImagesIDs: [BufferedImage] { return self.imagesIDs.enumerated().map { i, image in return BufferedImage(assetImageName: image, position: i) } } init(imagesIDs: [String]) { self.imagesIDs = imagesIDs } internal func makeBuffer(for centralIdx: Int, buffer: inout [BufferedImage]) { precondition(centralIdx >= 0 && centralIdx < self.imagesIDs.count) var tempBuffer: [BufferedImage] = [BufferedImage].init(repeating: .zero, count: 3) for i in -1...1 { let next = (centralIdx + i + imagesIDs.count) % imagesIDs.count tempBuffer[i+1] = BufferedImage(assetImageName: imagesIDs[next], position: i+1) } buffer = tempBuffer } func prepareForNext(forward: Bool = true) { let nextIdx = forward ? (currentImage+1)%imagesIDs.count : (currentImage-1+imagesIDs.count) % imagesIDs.count if forward { self.offsetImageSelector -= 1 } else { self.offsetImageSelector += 1 } assert(nextIdx >= 0 && nextIdx <= imagesIDs.count) self.makeBuffer(for: nextIdx, buffer: &self.offScreenBuffer) currentImage = nextIdx } func swapOnscreenBuffer() { self.onScreenBuffer = Array(self.offScreenBuffer) self.offsetImageSelector = 0 } internal struct BufferedImage: Identifiable { var assetImageName: String var position: Int var id: String init(assetImageName: String, position: Int) { self.assetImageName = assetImageName self.position = position self.id = "\(assetImageName)\(position)" } public static let zero = BufferedImage(assetImageName: "placeholder", position: .zero) } } ContentView.swift import SwiftUI struct ContentView: View { @StateObject private var carouselModel = CarouselModel( imagesIDs: (1..<6).map { i in return "PH\(i)" } ) @State private var dragOffset: CGFloat = .zero var body: some View { GeometryReader { geo in VStack { HStack(spacing: 0) { ForEach(carouselModel.onScreenBuffer, id: \.id) { bufferedImage in Image(bufferedImage.assetImageName) .resizable() .aspectRatio(16.0/9.0, contentMode: .fit) .frame(width: geo.size.width + geo.safeAreaInsets.leading + geo.safeAreaInsets.trailing, alignment: .leading) } } .offset( x: -(geo.size.width + geo.safeAreaInsets.leading + geo.safeAreaInsets.trailing - dragOffset)*CGFloat((1-carouselModel.offsetImageSelector)) ) .frame(maxWidth: geo.size.width + geo.safeAreaInsets.leading + geo.safeAreaInsets.trailing, alignment: .leading) .clipped() .overlay { ChevronButtonsOverlay(galleryWidth: geo.size.width + geo.safeAreaInsets.leading + geo.safeAreaInsets.trailing) } } } } @ViewBuilder func ChevronButton() -> some View { ZStack { Circle() .fill(.ultraThinMaterial) .frame(width: 35, height: 35) .background { Color(UIColor.label).opacity(0.8).clipShape(Circle()) } Image(systemName: "chevron.left") .font(.system(size: 15).weight(.black)) .blendMode(.destinationOut) .offset(y: -1) } .compositingGroup() } @ViewBuilder func ChevronButtonsOverlay(galleryWidth: CGFloat) -> some View { HStack(alignment: .center, spacing: 0) { Button(action: { if abs(carouselModel.offsetImageSelector) < 1 { withAnimation(.easeOut(duration: 0.25)) { carouselModel.prepareForNext(forward: false) self.dragOffset = galleryWidth } DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { self.dragOffset = 0 carouselModel.swapOnscreenBuffer() } } }) { ChevronButton() } Spacer() Button(action: { if abs(carouselModel.offsetImageSelector) < 1 { withAnimation(.easeOut(duration: 0.25)) { carouselModel.prepareForNext(forward: true) self.dragOffset = -galleryWidth } DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { self.dragOffset = 0 carouselModel.swapOnscreenBuffer() } } }) { ChevronButton() } .rotationEffect(.degrees(180)) } .padding(5) .frame(maxWidth: .infinity, maxHeight: .infinity) } }
Posted Last updated
.
Post not yet marked as solved
2 Replies
308 Views
I need to execute a code before the device's interface starts rotating but I can't seem to find any viable solution. I came across UIDevice.orientationDidChangeNotification but couldn't find anything like that to be notified that an orientation change will start. I tried creating an UIViewControllerRepresentable with a custom UIViewController that overrides func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator), but that function doesn't get invoked at all (I didn't forget to propagate it using super). Any idea?
Posted Last updated
.
Post marked as solved
1 Replies
519 Views
So, this is the context: I have a set of custom shapes, say for instance ShapeA, ShapeB, ShapeC, ShapeD, ShapeE, ShapeF, all of them conforming to a user defined protocol like this: protocol HasNormalizedAABB: Shape { func normalizedAABB() -> CGRect } It is very clear that HasNormalizedAABB also requires conformance to Shape, that in turn requires conformance to View, so HasNormalizedAABB concretions are also Views. Then somewhere else in the code there is an enum that determines which of those shapes is visible at the moment, say for the sake of this example: enum VisibleShape { case A case B case C case D case E case F } Now I need a way to map each possible state of this enum to its corresponding HasNormalizedAABB shape and organically apply a styling to whatever the rendered shape is. Keep in mind that I could need to do the same in 500 different views with 500 different styles applied to it. This makes it absolutely critical to create a way to parametrize the style instead of creating a different mapping for each style. This is what I tried: @ViewBuilder static func EnumToShape( enumState: VisibleShape, transform: @escaping (any HasNormalizedAABB) -> some View ) -> some View { switch enumState { case .A: transform(ShapeA()) case .B: transform(ShapeB()) case .C: transform(ShapeC()) case .D: transform(ShapeD()) case .E: transform(ShapeE()) case .F: transform(ShapeF()) } } Which is supposed to be used as follows: EnumToShape(enumState: myStateVariableOfTypeVisibleShape) { $0 .stroke(.green, lineWidth: 2) .shadow(color: .green, radius: 4) } Which causes Type 'any View' cannot conform to 'View'
Posted Last updated
.
Post not yet marked as solved
1 Replies
793 Views
So, I'm trying to learn basics of Core Media since I need to process real time audio samples in my app. For now I know that I need to configure an AVCaptureSession setting an AVCaptureDevice used to acquire samples and an AVCaptureDataOutput that processes the input from the device and "notifies" an AVCaptureAudioDataSampleBufferDelegate through a captureOutput(...) method. Now, this one method gets passed the samples as an CMSampleBuffer object, that according to Apple's CM documentation, will contain zero or more media (audio in my case) samples and a CMBlockBuffer, that is [...] a CFType object that represents a contiguous range of data offsets [...] across a possibly noncontiguous memory region. OK So this is kinda getting confusing. I'm not a native speaker and I'm struggling to understand what this is supposed to mean. Why do I need this to access my samples? Aren't they stored as an array of raw binary data (therefore homogeneous and contiguous)? I guess this is related to how the underlying memory is managed by Core Media but I can't figure it out. Also the last batch of samples gets accessed through this CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer  method which expects an unsafe mutable pointer to an AudioBufferList and one to an optional CMBlockBuffer. The first one will be filled with pointers into the latter, and then I may (or may not) be able to access the samples through myAudioBufferList.mBuffers.mData, which might be nil. Example code from Apple Developers code snippets: public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { var audioBufferList = AudioBufferList() var blockBuffer: CMBlockBuffer? CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer( sampleBuffer, bufferListSizeNeededOut: nil, bufferListOut: &audioBufferList, bufferListSize: MemoryLayout.stride(ofValue: audioBufferList), blockBufferAllocator: nil, blockBufferMemoryAllocator: nil, flags: kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, blockBufferOut: &blockBuffer) guard let data = audioBufferList.mBuffers.mData else { return } } What's the memory model (or pipeline) behind this? I truly appreciate any help.
Posted Last updated
.