UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: How should I refactor this View?

Forums > SwiftUI

Hi there, I need some help with this code.

I have this Model, with assuntos being Optional:

struct Documento: Identifiable, Codable {
    var id = UUID().uuidString
    let url: String
    let documento: String
    let ccvm: String
    let dataRef: String
    let frmDtRef: String
    let categoria: String
    let tipo: String
    let especie: String
    let situacao: String
    let assuntos: Assuntos?

    enum CodingKeys: String, CodingKey {
        case url, ccvm
        case documento = "Documento"
        case dataRef = "DataRef"
        case frmDtRef = "FrmDtRef"
        case categoria = "Categoria"
        case tipo = "Tipo"
        case especie = "Especie"
        case situacao = "Situacao"
        case assuntos = "Assuntos"
    }

}

struct Assuntos: Codable {
    let assunto: [String]

    enum CodingKeys: String, CodingKey {
        case assunto = "Assunto"
    }
}

And this View:

struct DocumentsView: View {
    @EnvironmentObject var viewModel: DocumentViewModel

    var body: some View {
        NavigationStack {
                List {
                    ForEach(viewModel.documentos) { item in
                        NavigationLink(destination: DocumentDetailView(title: item.ccvm, url: URL(string: item.url)!),
                                       label: {
                            LazyVStack(alignment: .leading) {
                                Text(companies[item.ccvm] ?? "❌")
                                    .font(.headline)
                                    .fontWeight(.bold)
                                    .lineLimit(1)
                                    .padding(.bottom, 8)
                                Text(item.categoria)
                                if let assuntos = item.assuntos.assunto {
                                    ForEach(assuntos, id: \.self) { assunto in
                                        Text(assunto)
                                            .font(.subheadline)
                                            .foregroundColor(.secondary)
                                            .lineLimit(1)
                                    }
                                }
                            }
                        })
                    }
                }
                .listStyle(PlainListStyle())
                .toolbar {
                    ToolbarItem(placement: .navigationBarLeading) {
                        let txtDataFormatted = viewModel.txtData.formatted(date: .abbreviated, time: .omitted)
                        Text("Data: \(txtDataFormatted)")
                            .font(.subheadline)
                            .foregroundColor(Color.red)
                    }
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button {
                            withAnimation(.linear(duration: 2.0)) {
                                viewModel.getDocuments()
                            }
                        } label: {
                            Image(systemName: "goforward")
                                .tint(.red)
                        }
                        .rotationEffect(Angle(degrees: viewModel.isLoading ? 360 : 0), anchor: .center)
                        .font(.title3)
                    }
                }
                .navigationTitle("Documentos")
                .alert(item: $viewModel.appError) { appAlert in
                    Alert(title: Text("Error"), message: Text("\(appAlert.errorString)\nPlease try again later!"))
                }
        }
        .overlay(loadingOverlay)
        .onShake {
            viewModel.documentos.removeAll()
        }
    }

    @ViewBuilder private var loadingOverlay: some View {
        if viewModel.isLoading {
            ProgressView()
                .progressViewStyle(CircularProgressViewStyle(tint: .red))
        }
    }
}

But the compiler gives me this error message:

The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions.

The funny thing is that if I do not make assuntos Optional, the code compiles normally. I guess the best option would be to refactor the code above, but I have no idea how to implement this refactoring.

By the way, this is the VM:

class DocumentViewModel: ObservableObject {
    @Published var documentos: [Documento] = []
    @Published var txtLogin = "XXX"
    @Published var txtSenha = "YYY"
    @Published var txtData = Date()
    @Published var txtHora = "00:00"
    @Published var txtDocumento = "IPE"
    @Published var txtAssuntoIPE = "Sim"
    @Published var isLoading: Bool = false

    var appError: AppError? = nil

    struct AppError: Identifiable {
        let id = UUID().uuidString
        let errorString: String
    }

    func getDocuments() {
        isLoading = true
        HapticManager.notification(type: .success)

        let formatter: DateFormatter = {
            let formatter = DateFormatter()
            formatter.dateFormat = "dd/MM/yyyy"
            return formatter
        }()

        let body = """
            txtLogin=\(txtLogin)&txtSenha=\(txtSenha)&txtData=\(formatter.string(from: txtData))&txtHora=\(txtHora)&txtAssuntoIPE=\(txtAssuntoIPE)&txtDocumento=\(txtDocumento)
            """

        let apiService = APIService.shared
        apiService.fetchDocuments(urlString: "http://seguro.bmfbovespa.com.br/rad/download/SolicitaDownload.asp", body: body) { (result: Result<[Documento], APIService.APIError>) in
            switch result {
            case .success(var documentos):
                documentos = documentos.filter({ $0.situacao == "Liberado" })
                self.isLoading = false
                self.documentos = documentos
                print(documentos)
            case .failure(let apiError):
                switch apiError {
                case .error(let errorString):
                    self.isLoading = false
                    self.appError = AppError(errorString: errorString)
                    print(errorString)
                }
            }
        }
    }
}

And this is the APIService:

class APIService {
    static let shared = APIService()
    var cancellables = Set<AnyCancellable>()

    enum APIError: Error {
        case error(_ errorString: String)
    }

    func fetchDocuments<T: Decodable>(urlString: String,
                              body: String,
                              dateDecodingStrategy: XMLDecoder.DateDecodingStrategy = .deferredToDate,
                              keyDecodingStrategy: XMLDecoder.KeyDecodingStrategy = .useDefaultKeys,
                              completion: @escaping (Result<T, APIError>) -> Void) {

        guard let url = URL(string: urlString) else {
            completion(.failure(.error(NSLocalizedString("Error: Invalid URL.", comment: ""))))
            return
        }

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.httpBody = Data(body.utf8)

        let decoder = XMLDecoder()
        decoder.dateDecodingStrategy = dateDecodingStrategy
        decoder.keyDecodingStrategy = keyDecodingStrategy

        URLSession
            .shared
            .dataTaskPublisher(for: request)
            .map(\.data)
            .decode(type: T.self, decoder: decoder)
            .receive(on: RunLoop.main)
            .sink { taskCompletion in
                switch taskCompletion {
                case .finished:
                    print("Finished")
                    return
                case .failure(let decodingError):
                    completion(.failure(APIError.error("Error: \(decodingError.localizedDescription)")))
                }
            } receiveValue: { decodedData in
                completion(.success(decodedData))
            }
            .store(in: &cancellables)
    }
}

Thanks for the support.

2      

hi Rafael,

on refactoring, the easiest things to do are to pull out the deepest-level constructs that are the content of the LazyVStack and the two ToolbarItems as separate functions or views.

for example, you could write

ToolbarItem(placement: .navigationBarTrailing, content: trailingButton)

where you define the function trailingButton within the DocumentsView to be

func trailingButton() -> some View {
  Button {
    withAnimation(.linear(duration: 2.0)) {
      viewModel.getDocuments()
    }
  } label: {
    Image(systemName: "goforward")
      .tint(.red)
  }
    .rotationEffect(Angle(degrees: viewModel.isLoading ? 360 : 0), anchor: .center)
      .font(.title3)
}

hope that gets you off to a good start,

DMG

2      

Hi DMG,

I implemented that refactoring of yours, but still receiving the same error message.

I guess the code part that should be refactored is the if let assuntos block...

Any idea on this one?

Thanks a lot!

2      

hi,

keep refactoring ... there are still two low-level constructs to go ... but you could also try commenting out some code and checking compilation to see what's really driving the compiler crazy.

for example, how about trying

ForEach(viewModel.documentos) { item in
    // comment out the body, but replace with
    Text(item.documento)
}

if this compiles, then you know that you focus on refactoring the content of the ForEach; and you'll at least be giving SwiftUI a better chance to eventually expose whatever's causing the problem.

hope that helps,

DMG

2      

In addition to just factoring out some of the structural parts of your View, I would consider adjusting your model code as well in order to eliminate the need for this kind of code:

if let assuntos = item.assuntos.assunto {
    ForEach(assuntos, id: \.self) { assunto in
        Text(assunto)
            .font(.subheadline)
            .foregroundColor(.secondary)
            .lineLimit(1)
    }
}

There are a couple different ways to approach this.

The easiest way would be to just give Documento a computed property that turns the optional assuntos property into a non-optional array:

var assuntosArray: [Assunto] {
    assuntos ?? []
}

and then use that computed property in the ForEach instead of having to unwrap an optional.

Another way to do it would be to rework how you decode the JSON. Why have an assuntos property that is an array of structs that only contain a single String? Why not just make it an array of Strings?

Can you post a sample of the JSON you are decoding for your Documento struct?

2      

Hi roosterboy,

There are different ways I can reach this API (which returns a XML file, that is why I use the XMLCoder library, examples below).

I can ask the API to return Documents with Assuntos (which means Documents with Subjects) or I can ask it to return Documents without Subjects, that is why my Model has optional Assuntos.

Example 1, with subjects:

<DownloadMultiplo DataSolicitada="21/10/2022 00:00" TipoDocumento="IPE" DataConsulta="22/10/2022 21:14">
<Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=962706" Documento="IPE" ccvm="521027" DataRef="31/12/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Dados Econômico-Financeiros" Tipo="Demonstrações Financeiras Anuais Completas" Especie=" " Situacao="Cancelado">
<Assuntos Quantidade="0"/>
</Link>
<Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=1022249" Documento="IPE" ccvm="8036" DataRef="14/10/2022 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Aviso aos Debenturistas" Tipo=" " Especie=" " Situacao="Cancelado">
<Assuntos Quantidade="1">
<Assunto>
Pagamento de proventos das Debênture da 15ª emissão / 1ª série
</Assunto>
</Assuntos>
</Link>
<Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=1023559" Documento="IPE" ccvm="50571" DataRef="21/10/2022 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado">
<Assuntos Quantidade="1">
<Assunto>424B2</Assunto>
</Assuntos>
</Link>
<Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=1023560" Documento="IPE" ccvm="51713" DataRef="21/10/2022 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado">
<Assuntos Quantidade="1">
<Assunto>10-Q</Assunto>
</Assuntos>
</Link>
<Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=1023561" Documento="IPE" ccvm="56600" DataRef="21/10/2022 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado">
<Assuntos Quantidade="1">
<Assunto>6-K</Assunto>
</Assuntos>
</Link>

Example 2, without subjects:

<DownloadMultiplo DataSolicitada="21/10/2022 00:00" TipoDocumento="IPE" DataConsulta="22/10/2022 21:19">
<Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=962706" Documento="IPE" ccvm="521027" DataRef="31/12/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Dados Econômico-Financeiros" Tipo="Demonstrações Financeiras Anuais Completas" Especie=" " Situacao="Cancelado"/>
<Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=1022249" Documento="IPE" ccvm="8036" DataRef="14/10/2022 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Aviso aos Debenturistas" Tipo=" " Especie=" " Situacao="Cancelado"/>
<Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=1023559" Documento="IPE" ccvm="50571" DataRef="21/10/2022 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"/>
<Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=1023560" Documento="IPE" ccvm="51713" DataRef="21/10/2022 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"/>
<Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=1023561" Documento="IPE" ccvm="56600" DataRef="21/10/2022 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"/>

I have a SettingsView that I use to make a POST request to the API, with a boolean field sending Yes/No for Subjects.

Thanks for your help.

2      

Hi https://www.hackingwithswift.com/users/roosterboy ,

Any ideas on how I should handle this problem?

Thank you again!

2      

Nothing that I and others haven't already mentioned.

I'm not familiar with the XMLCoder library, so I can't offer any suggestions how to rework the way you decode the XML returned by the API.

2      

Hi @roosterboy and @delawaremathguy,

I managed to figured the solution out. The problem was in the Documento model, the Struct Assuntos should be inside the Struct Documentos, then I could mark Assuntos as optional normally.

Then in DocumentsView, I've marked assuntos as optional as well.

if let assuntos = item.assuntos?.assunto

Now I can send different POST requests to the API, asking for Documents with or without subjects...the List inside DocumentsView updates accordingly.

Best,

2      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.