In questo articolo, esploreremo come costruire un’applicazione mobile iOS semplice che visualizza una citazione casuale da un elenco di citazioni memorizzate in un file di testo online. Questo esercizio ci permetterà di imparare a utilizzare alcune importanti tecnologie Swift, tra cui SwiftUI per l’interfaccia utente e Combine per la gestione degli eventi asincroni.
Iniziamo descrivendo l’architettura generale del nostro progetto. L’applicazione sarà composta da una singola schermata che visualizza una citazione casuale e offre un pulsante per caricare una nuova citazione. Le citazioni saranno memorizzate in un file di testo online, con una particolare formattazione per separare ogni citazione.
Partiamo da una semplice applicazione SwiftUI. Creiamo una nuova classe Cancellables
per tenere traccia dei nostri processi asincroni. Utilizzeremo la libreria Combine di Swift per gestire i processi asincroni e i cambiamenti di stato.
import SwiftUI
import Combine
class Cancellables: ObservableObject {
var set = Set<AnyCancellable>()
}
Successivamente, creiamo il nostro ContentView
, che è il componente principale della nostra applicazione.
struct ContentView: View {
@State private var text = "Loading..."
private let url = URL(string: "https://www.raucci.net/frasi.txt")!
@StateObject private var cancellables = Cancellables()
@State private var rotation = 0.0
Abbiamo definito alcune variabili di stato: text
per la citazione attuale, url
per l’URL del nostro file di citazioni, cancellables
per conservare i processi asincroni e rotation
per gestire l’animazione dell’immagine.
A seguire, definiamo la struttura visiva del nostro ContentView
.
var body: some View {
NavigationView {
VStack {
Image("yourImageName")
.resizable()
.aspectRatio(contentMode: .fit)
.scaleEffect(0.8)
.rotationEffect(.degrees(rotation))
.onTapGesture {
fetchQuotes()
withAnimation(.easeInOut(duration: 1.0)) {
self.rotation += 360
}
}
Text(text)
.font(.custom("jgs_Font", size: 38))
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.navigationTitle("fortune...")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: copyQuote) {
Image(systemName: "doc.on.doc")
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: fetchQuotes) {
Image(systemName: "arrow.clockwise")
}
}
}
}
.onAppear {
fetchQuotes()
}
}
Qui abbiamo definito una NavigationView
con una VStack
che contiene un’immagine e un Text
. L’immagine è configurata per ridimensionarsi, mantenere le proporzioni, ridursi all’80% della sua dimensione originale e ruotare in base alla variabile di stato rotation
. Il Text
visualizza la citazione attuale e ha un font personalizzato, del padding e viene centrato nella vista. Abbiamo anche una barra di navigazione con due pulsanti: uno per copiare la citazione attuale e l’altro per caricare una nuova citazione.
La funzione fetchQuotes
viene chiamata quando l’utente tocca l’immagine o il pulsante di caricamento e quando la vista appare.
private func copyQuote() {
UIPasteboard.general.string = text
}
La funzione copyQuote
copia la citazione attuale negli appunti del dispositivo.
private func fetchQuotes() {
URLSession.shared.dataTaskPublisher(for: url)
.map { data, _ in String(data: data, encoding: .utf8) }
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
if case .failure(let err) = completion {
self.text = "Failed to load: \(err)"
}
}, receiveValue: { text in self.processText(text) })
.store(in: &cancellables.set)
}
fetchQuotes
avvia un processo asincrono per caricare il contenuto del nostro file di citazioni. Una volta che il contenuto è stato caricato, viene invocata la funzione processText
.
private func processText(_ text: String?) {
guard let text
= text else {
self.text = "Failed to load"
return
}
let trimmedText = text.replacingOccurrences(of: "\n", with: " ")
let components = trimmedText.components(separatedBy: "#-")
var phrases: [String] = []
for component in components.dropFirst() {
if let endRange = component.range(of: "-#") {
let phrase = String(component[..<endRange.lowerBound]).trimmingCharacters(in: .whitespacesAndNewlines)
if !phrase.isEmpty {
phrases.append(phrase)
}
}
}
if phrases.isEmpty {
self.text = "No phrases found"
} else {
self.text = phrases.randomElement() ?? "No phrases found"
}
}
}
La funzione processText
processa il contenuto del file di citazioni, estrae le singole citazioni e seleziona una citazione casuale.