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.
Post
Replies
Boosts
Views
Activity
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'
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?
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)
}
}
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)?
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.
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.