Quero entender a utilidade de usar AsyncStream
quando o iOS 17 introduziu @Observable
a macro onde podemos observar diretamente as mudanças no valor de qualquer variável no modelo (e o rastreamento de observação pode acontecer mesmo fora da visualização SwiftUI). Então, se eu estiver observando um fluxo contínuo de valores, como o progresso do download de um arquivo usando AsyncStream
uma visualização SwiftUI, o mesmo pode ser observado na mesma visualização SwiftUI usando onChange(of:initial)
o progresso do download (armazenado como uma propriedade no objeto do modelo). Estou procurando benefícios, desvantagens e limitações de ambas as abordagens.
Especificamente, minha pergunta é com relação ao código de exemplo AVCam da Apple, onde eles observam alguns estados como segue. Isso é feito em CameraModel
classe que é anexada à visualização SwiftUI.
// MARK: - Internal state observations
// Set up camera's state observations.
private func observeState() {
Task {
// Await new thumbnails that the media library generates when saving a file.
for await thumbnail in mediaLibrary.thumbnails.compactMap({ $0 }) {
self.thumbnail = thumbnail
}
}
Task {
// Await new capture activity values from the capture service.
for await activity in await captureService.$captureActivity.values {
if activity.willCapture {
// Flash the screen to indicate capture is starting.
flashScreen()
} else {
// Forward the activity to the UI.
captureActivity = activity
}
}
}
Task {
// Await updates to the capabilities that the capture service advertises.
for await capabilities in await captureService.$captureCapabilities.values {
isHDRVideoSupported = capabilities.isHDRSupported
cameraState.isVideoHDRSupported = capabilities.isHDRSupported
}
}
Task {
// Await updates to a person's interaction with the Camera Control HUD.
for await isShowingFullscreenControls in await captureService.$isShowingFullscreenControls.values {
withAnimation {
// Prefer showing a minimized UI when capture controls enter a fullscreen appearance.
prefersMinimizedUI = isShowingFullscreenControls
}
}
}
}
Se virmos a estrutura CaptureCapabilities
, é uma estrutura pequena com dois membros Bool. Essas mudanças poderiam ter sido observadas diretamente por uma visualização SwiftUI. Gostaria de saber se há uma vantagem ou razão específica para usar AsyncStream
aqui e iterar continuamente sobre as mudanças em um loop for.
/// A structure that represents the capture capabilities of `CaptureService` in
/// its current configuration.
struct CaptureCapabilities {
let isLivePhotoCaptureSupported: Bool
let isHDRSupported: Bool
init(isLivePhotoCaptureSupported: Bool = false,
isHDRSupported: Bool = false) {
self.isLivePhotoCaptureSupported = isLivePhotoCaptureSupported
self.isHDRSupported = isHDRSupported
}
static let unknown = CaptureCapabilities()
}
Algumas perguntas aqui:
Para responder à pergunta no abstrato,
AsyncSequence
(do qualAsyncStream
é apenas um exemplo concreto) é um padrão mais geral. Quando implementado corretamente, suporta cancelamento, produz valores assincronamente ao longo do tempo, e onde necessário, e tem a noção de "terminar" quando a sequência é feita. Os padrõesObservableObject
/@Observable
são um padrão mais estreito (notavelmente,@Observable
é ideal para SwiftUI, um pouco incômodo em UIKit/AppKit) para publicar alterações de algum estado de um objeto. Para seu exemplo específico (para informar SwiftUI sobre alterações de estado ao longo do tempo), tanto a observação quanto as sequências assíncronas podem realizar o trabalho; eu não perderia muito sono em relação a uma ou outra.Padrões relacionados à observação são bem naturais quando uma IU quer ser atualizada com base no estado de um objeto. É um pouco menos natural quando você quer que algum objeto (como este serviço de “câmera”) atualize seu estado com base em outros eventos (embora você possa fazer isso). Não vejo nenhuma falha inerente no padrão de sequência assíncrona empregado aqui. Mas você pode adotar qualquer um dos padrões aqui. Na verdade, eles estão apenas criando sequências assíncronas de publicadores usando
values
, então é um pouco uma mistura de ambos.Se você for usar sequências assíncronas, devemos observar que toda a concorrência não estruturada nisso
observeState
é uma implementação questionável. Ela está iniciando quatro tarefas não estruturadas sem nenhuma maneira de cancelá-las.Claro, eles estão lançando isso do
AVCamApp
, então, neste caso, eles não estão pensando em parar oCameraModel
, mas eu realmente desencorajaria esse padrão. Não se deve assar em comportamento não cancelável, especialmente em projetos de demonstração que os desenvolvedores podem estar inclinados a copiar e/ou incorporar em seus projetos.Geralmente, salvaríamos referências a essas tarefas e, então, forneceríamos uma função
stop
/cancel
para cancelá-las. Ou, neste caso, melhor, perder a concorrência não estruturada, encapsular essas quatro em um grupo de tarefas:Então, mudaríamos
start
(que eu renomeariarun
) para fazer isso por último:Então você
View
poderia fazer algo como:Agora, novamente, o exemplo de código original estava fazendo isso no
App
, então esse problema não se manifestou lá. Mas o acima seria uma solução mais generalizada, funcionando tanto em anApp
quanto em aView
.O uso do código original de toda essa simultaneidade não estruturada e a falha em contemplar o cancelamento é um antipadrão. Queremos
CameraModel
poder ser usados de umApp
ou de umView
. (Vou ignorar seCameraModel
é um bom nome para esse objeto ou não; parece mais um “serviço” do que um simples “modelo”, na minha humilde opinião.)