A inicialização repetida de linhas SwiftUI não prova que uma linha desapareceu ou perdeu seu estado. Para diagnosticar uma interface de Mac com rolagem, acompanhe separadamente a construção, o estado retido, os callbacks de aparição e a visibilidade na área de visualização.

Este passo a passo é para um desenvolvedor que investiga logs repetidos de linhas em seu próprio app macOS. A tarefa concreta é determinar se uma atualização apenas cria outro valor de view, redefine o estado da linha ou altera quais linhas ocupam a área de visualização da rolagem. O exemplo usa rótulos sintéticos e botões comuns.

O que cada sinal do ciclo de vida realmente informa?

Uma linha SwiftUI normalmente usa uma estrutura, que é um tipo de valor do Swift. Construir outro valor de estrutura é diferente de criar um objeto de referência persistente. A documentação do Swift explica essa distinção em Tipos de valor e de referência no Swift.

O SwiftUI acrescenta seu próprio modelo de identidade. A Apple explica que valores temporários de view podem representar a mesma identidade entre atualizações e que o ciclo de vida de uma view acompanha essa identidade. Portanto, um log do inicializador não comprova que o ciclo de vida da linha anterior terminou. Consulte Desmistificando o SwiftUI.

Use estes sinais para responder a perguntas diferentes:

Sinal Pergunta respondida Limite de interpretação
Inicializador da linha Outro valor de linha foi construído? Não comprova aparição nem substituição do estado.
Avaliação de body Esta descrição de view foi avaliada? Não conta os quadros exibidos.
Estado retido da linha Este valor de diagnóstico sobreviveu à atualização? Descreve a continuidade desse estado, não a ocupação da área de visualização.
onAppear e onDisappear O SwiftUI informou uma transição de aparição? O momento depende do tipo de view.
onScrollVisibilityChange A linha cruzou um limite escolhido da área de visualização? Mede a condição documentada de visibilidade na rolagem.

A Apple documenta que onAppear é concluído antes do primeiro quadro renderizado, e o momento exato depende do tipo de view. onDisappear é executado após o desaparecimento, também em um momento que depende da view. Nenhum dos métodos documenta a desalocação do estado. Mantenha esses eventos de callback separados das conclusões sobre o tempo de vida do armazenamento. Consulte onAppear(perform:) e onDisappear(perform:).

Para investigar a área de visualização, escolha explicitamente um limite. No macOS 15 e posterior, onScrollVisibilityChange(threshold:_:) informa se a view cruza o limite de visibilidade fornecido; o padrão é 0.5. A Apple também documenta um callback inicial quando a view aparece e o limite já foi atingido. Consulte onScrollVisibilityChange(threshold:_:).

Neste artigo, “visível na área de visualização” significa satisfazer esse limite de rolagem. Isso não mede se alguém olhou para a linha.

Crie um pequeno experimento que separe os sinais

Crie um app SwiftUI para macOS usando um SDK que inclua as APIs do macOS 15 e defina o destino de implantação como macOS 15 ou posterior. Substitua as declarações geradas do app e da view de conteúdo pelo programa completo abaixo. Mantenha apenas um ponto de entrada @main.

O exemplo usa ScrollView e LazyVStack para tornar explícito o comportamento da área de visualização. A Apple descreve LazyVStack como uma estrutura que cria itens conforme necessário. Este experimento não pressupõe que List tenha comportamento idêntico de carregamento ou retenção. Consulte LazyVStack.

import SwiftUI
import Foundation

@main
struct RowLifetimeLabApp: App {
    var body: some Scene {
        WindowGroup("Row Lifetime Lab") {
            ProbeView()
        }
    }
}

struct ProbeView: View {
    @State private var revision = 0
    @State private var generation = 0

    var body: some View {
        VStack(spacing: 12) {
            HStack {
                Button("Update rows") {
                    revision += 1
                }
                Button("Reset identity") {
                    generation += 1
                }
                Text("Revision \(revision)")
            }

            ScrollView {
                LazyVStack(spacing: 8) {
                    ForEach(1...60, id: \.self) { rowID in
                        ProbeRow(rowID: rowID, revision: revision)
                    }
                }
                .padding()
            }
            .id(generation)
        }
        .padding()
        .frame(minWidth: 560, minHeight: 360)
    }
}

struct ProbeRow: View {
    let rowID: Int
    let revision: Int
    private let constructionID: UUID

    @State private var appearanceToken: UUID? = nil
    @State private var count = 0

    init(rowID: Int, revision: Int) {
        self.rowID = rowID
        self.revision = revision
        self.constructionID = UUID()
        print("init row=\(rowID) value=\(constructionID)")
    }

    var body: some View {
        let _ = print("body row=\(rowID) value=\(constructionID)")

        HStack {
            VStack(alignment: .leading) {
                Text("Sample row \(rowID)")
                Text("Revision \(revision)")
                Text(appearanceToken?.uuidString ?? "Awaiting appearance")
                    .font(.caption.monospaced())
            }
            Spacer()
            Button("Count \(count)") {
                count += 1
            }
        }
        .padding(12)
        .frame(height: 80)
        .background(Color.gray.opacity(0.12))
        .onAppear {
            if appearanceToken == nil {
                appearanceToken = UUID()
            }
            print("appear row=\(rowID) token=\(tokenDescription)")
        }
        .onDisappear {
            print("disappear row=\(rowID) token=\(tokenDescription)")
        }
        .onScrollVisibilityChange(threshold: 0.5) { visible in
            print("viewport row=\(rowID) visible=\(visible)")
        }
    }

    private var tokenDescription: String {
        appearanceToken?.uuidString ?? "unassigned"
    }
}

Os dois UUIDs têm funções diferentes. constructionID identifica cada invocação do inicializador. appearanceToken começa vazio e recebe um valor na primeira aparição observada para o estado retido da linha. Ele não marca o momento de alocação do estado. A contagem fornece outro valor visível cuja continuidade você pode inspecionar.

A Apple documenta que o SwiftUI gerencia o armazenamento de State e atualiza o conteúdo da interface dependente quando seu valor muda. Por isso, o diagnóstico mantém o token e a contagem em estado privado, enquanto as informações de construção permanecem em uma propriedade armazenada comum. Consulte State.

O programa foi conferido em relação às APIs documentadas, mas não foi compilado nem executado aqui porque não havia um toolchain do Swift disponível. Os exemplos de log abaixo descrevem como interpretar sua execução; não são resultados capturados.

Execute três verificações controladas e interprete os resultados

1. Atualize os dados das linhas sem alterar a identidade

Execute o app no Xcode e mantenha o console de depuração aberto. Clique duas vezes no botão Count de uma linha visível, anote seu token e depois clique em Update rows. A revisão passada às linhas muda, enquanto os identificadores inteiros fixos permanecem iguais.

Se as mensagens do inicializador aparecerem de novo, mas a linha selecionada mantiver seu token e sua contagem, há evidência de construção repetida de valores com estado de diagnóstico retido. Não classifique essas mensagens do inicializador como ciclos de vida distintos na tela. A revisão exibida confirma que a linha recebeu o novo dado.

O token de aparição é atribuído por meio do estado, o que pode causar outra atualização. Esse comportamento de diagnóstico é intencional. Não espere um número exato de mensagens do inicializador ou de body e remova essas sondagens quando a investigação terminar.

2. Role uma linha para fora da área de visualização e traga-a de volta

Role devagar até a linha selecionada ultrapassar o limite de 50%. Compare as mensagens de viewport com as mensagens de aparição e depois volte à linha. Inspecione novamente o token e a contagem.

Um token retido ao voltar demonstra a continuidade desse estado de diagnóstico durante essa saída da área de visualização. Um token diferente significa que o token anterior não foi retido. Para explicar uma redefinição em um app real, investigue alterações de identidade, remoção condicional e o comportamento de retenção do contêiner, em vez de inferir a causa apenas com base na rolagem.

As orientações atuais da Apple explicam que pilhas lazy podem preparar o corpo e o layout de uma view antes da aparição. Se a rolagem for revertida, uma view pré-carregada talvez nunca receba onAppear. A Apple também explica que o estado de views fora da tela em pilhas lazy pode acabar sendo descartado e recomenda manter em um modelo ou em uma view externa os dados que precisam sobreviver à rolagem. Consulte Explore pilhas lazy e rolagem com SwiftUI.

Isso torna a contagem local à linha do exemplo uma sonda útil. Em uma interface de edição real, armazene em um proprietário com ciclo de vida mais longo as alterações que precisam sobreviver à remoção da linha, usando o identificador estável do item como chave.

3. Redefina a identidade de propósito

Clique em Reset identity. O programa altera a identidade associada a toda a view de rolagem, oferecendo uma comparação deliberada com a atualização comum da revisão. A Apple documenta explicitamente que alterar o valor fornecido a id(_:) redefine a identidade da view, inclusive seu estado. Consulte id(_:).

Inspecione as linhas que reaparecem para verificar se têm tokens novos e contagens iniciais. Como a redefinição se aplica à subárvore da view de rolagem, não a use como teste controlado de preservação da posição de rolagem.

Use esta referência ao revisar seu próprio rastreamento:

Observação Conclusão sustentada Próxima verificação
Novas mensagens do inicializador; token e contagem persistem Houve construção enquanto o estado de diagnóstico persistiu. Inspecione o custo do inicializador se as atualizações forem lentas.
Callbacks de aparição se repetem; token persiste A aparição repetida não substituiu esse token. Use callbacks de visibilidade na área de visualização para comportamentos que dependam de um limite.
Token e contagem são redefinidos após uma atualização comum O estado da linha não sobreviveu como esperado. Inspecione os IDs dos dados, os modificadores id alterados e os ramos condicionais.
Uma linha é inicializada, mas nunca informa aparição A construção, por si só, não comprovou a aparição. Considere a preparação lazy e mudanças posteriores na rolagem.
A visibilidade na área de visualização passa a ser falsa O limite de rolagem escolhido deixou de ser satisfeito. Decida se esse limite corresponde ao comportamento desejado.

Para dados dinâmicos, mantenha os identificadores associados aos itens subjacentes durante as atualizações. Um identificador gerado novamente a cada leitura impede a continuidade. Inspecione também as identidades ancestrais: IDs estáveis de linha não preservam uma linha dentro de uma subárvore cuja identidade você substitui deliberadamente. A Apple aborda identificadores estáveis de dados e identidade estrutural em Desmistificando o SwiftUI.

Qual é o caminho compatível em versões anteriores do macOS?

O modificador de visibilidade na rolagem do exemplo requer macOS 15. Reduzir o destino de implantação exige um ramo de disponibilidade com uma implementação separada; onAppear não é uma medição equivalente de limite.

Se seu app já tiver uma interface de rolagem do AppKit, NSScrollView.documentVisibleRect fornece a parte da view de documento visível pela view de conteúdo, expressa em coordenadas da view de documento. Uma alternativa baseada em geometria é comparar esse retângulo com os retângulos das linhas no mesmo sistema de coordenadas, recalculando após alterações de rolagem e layout. Consulte documentVisibleRect.

Por exemplo, uma regra escolhida explicitamente poderia classificar uma linha como visível quando pelo menos metade de sua área retangular intersecta o retângulo visível do documento. Essa é uma regra de aplicação proposta, não uma afirmação de que reproduz a implementação do SwiftUI. Trate explicitamente os retângulos vazios e a conversão de coordenadas.

O SwiftUI fornece NSViewRepresentable para integrar uma view do AppKit que seu app cria e gerencia. Seus métodos documentados de criação, atualização, coordenação e desmontagem oferecem uma ponte compatível. Respeite o controle do layout da view representada pelo SwiftUI; a Apple recomenda não alterar diretamente o quadro ou os limites dessa view. Consulte NSViewRepresentable.

Essa alternativa diz respeito a uma view de documento do AppKit que você controla. Ela não exige suposições sobre as views internas privadas de um List do SwiftUI.

O layout do TextKit comprova que o texto está na tela?

O TextKit exige a mesma separação entre preparação e geometria da área de visualização. Isso importa se a sua linha contiver uma view de texto do AppKit e a pergunta passar a ser quais segmentos de texto intersectam a área visível.

O NSTextViewportLayoutController do TextKit 2, disponível no macOS 12 e posterior, gerencia uma área de layout ativa que em geral inclui a área visível mais uma região de rolagem excedente. Seu viewportBounds inclui a área excedente, e seu viewportRange opcional descreve o layout atual da área de visualização. Nenhum deles deve ser reinterpretado como um conjunto exato de caracteres visíveis. Consulte NSTextViewportLayoutController.

Em uma implementação do TextKit 2, use a geometria do layout de texto em vez de eventos de alocação. NSTextLayoutManager pode enumerar segmentos de texto para um intervalo de texto fornecido. Compare os retângulos dos segmentos resultantes com o retângulo de recorte real após colocá-los no mesmo sistema de coordenadas. Leve em conta o textContainerOrigin da view de texto; não suponha que as coordenadas do contêiner de texto sejam iguais às coordenadas da view de documento. Consulte enumerateTextSegments(in:type:options:using:) e textContainerOrigin.

Em uma implementação existente do TextKit 1, NSLayoutManager.glyphRange(forBoundingRect:in:) aceita um retângulo nas coordenadas do contêiner de texto. A Apple informa que o intervalo contíguo retornado pode incluir glifos fora desse retângulo. Portanto, trata-se de um intervalo candidato para inspeção adicional, não de uma lista exata de visibilidade. Consulte glyphRange(forBoundingRect:in:).

Preserve o mecanismo de texto durante o diagnóstico. A Apple documenta que acessar a propriedade layoutManager de um NSTextView ou contêiner de texto no macOS 12 e posterior causa uma mudança para o modo de compatibilidade que usa NSLayoutManager. Para um caminho com TextKit 2, use textLayoutManager e suas APIs. Consulte NSTextView.

Escolha o sinal que responde à pergunta sobre a interface: logs do inicializador para construção, estado retido para continuidade, callbacks de aparição para transições de aparição e geometria da área de visualização para visibilidade. Um experimento com IDs estáveis e uma redefinição deliberada de identidade torna essas diferenças observáveis sem precisar adivinhar com base no volume de saída do console.

Fontes