(9.1.2021)
Minun herbaario- ja valokuvasovellukseni ovat melko laajoja
ohjelmistoja, joissa on 15-20 erillistä näkymää (View). Jotta
niiden käsittely olisi johdonmukaista ja kevyttä, tein
selvittelyjen jälkeen aika omanlaisen ratkaisun, millaista en ole
missään nähnyt. Sen kuvaus löytyy täältä: sovelluksen arkkitehtuuri.
Käytän sovellustyyppiä "SwiftUI Lifecycle", joten kaikki esimerkkinikin on tehty siltä pohjalta.
SOVELLUKSEN LOPETUS
Herbaariosovellusta lopetetettaessa haluan, että käyttäjä saa
eteensä ilmoituksen, jos tietokantaa on muutettu. Ilmoituksessa
kehoitetaan ottamaan tietokannasta varmuuskopio talteen. Jos
sovellus lopetetaan itse lisäämästäni painikkeesta "Lopetus", niin
koodiin on helppo järjestää Alert-ikkunan näyttäminen, ja sen
OK-painikkeesta sovellus suljetaan exit(0)-komennolla. Tämä
exit(0) sulkee sovelluksen kokonaan, eikä jätä sitä Applen
käyttöliittymästandardin tarkoittamaan valmiustilaan (kun
esimerkiksi suljet selaimen, jää ikkunan alalaidassa piste
selaimen kuvakkeen alle, eli selainsovellus on valmiustilassa).
Jos sovelluksen sulkee punaisesta ruksista, se jää valmiustilaan.
Vasemman ylänurkan punaisen ruksin klikkaukseen pääsee kiinni .onDisappear(perform:
{}) -muuttajalla (view modifier). Sen koodi käynnistyy
heti kun punaista ruksia on klikattu. Mutta osoittautui, että
sovellusta on siinä vaiheessa jo suljettu niin paljon, että
Alert-ikkunaa ei enää voi avata. Sopivaa olisi tässä vaiheessa
avata näytön oikeaan ylänurkkaan ilmoitus (notification), joita
eri sovellukset lähettävät. Mutta sen ohjelmointi joko on vielä
buginen tai huonosti dokumentoitu, enkä saanut toimivaa koodia
aikaiseksi. Totesin lopuksi, että unix-komentoja voi kyllä
käynnistää, ja testasin että sain say-komennolla ilmoituksen
puhuttuna. Tein siihen lopulta oman infonäkymän, joka avautuu
sheettinä.
Lopulta päätin viis veisata Applen standardista, ja sovellukseni
aina sulkeutuessaan näyttää tarvittaessa muistutuksen, ja aina
sulkeutuu kokonaan komentoon exit(0).
PERUSNÄKYMIÄ
Eräiden ominaisuuksien koodaaminen SwiftUI:lla kävi hämmästyttävän helposti ja nopeasti ja tuloksena olevassa koodissa on hyvin vähän rivejä. Vastaavia olin aikoinaan tutkinut ja selvitellyt melko pitkästi sekä C#:ssä (WinForm) että XIB-Swiftissä, mutta C# WPF oli jo helpompi. Tässä kyseiset tapaukset:
(7.3.2021)
SwiftUI:n sanotaan olevan lähtökohdaltaan näytönkuvailukieli. Näin
se pitkälti onkin. Jotta koodia ajettaessa saa esille mitään
(järkevää), on kaiken datan oltava valmiina vähintään
alkuarvoissaan. Kun data muuttuu, niin näyttö muuttuu välittömästi
sen mukaisesti.
Alkuarvoja voi asettaa muuttujan määrittelyn yhteydessä,
ympäristömuuttujina tai globaaleina muuttujina. Lisäksi
yksittäisessä näkymätiedostossa saa olla osuus .onAppear() {},
joka suoritetaan aluksi. Mistään en ole löytänyt selostusta, mutta
kokemuksen mukaan logiikka kulkee näin yksittäisessä
struct-tiedostossa:
- struct:n muuttujat (@State jne.) alustetaan alkuarvoihinsa
- body (näkymämäärittely) käydään läpi menemättä kuitenkaan
varsinaisiin funktioihin mutta mennään kutsuttuihin muihin
struct-tiedostoihin
- suoritetaan .onAppear()
- body käydään tarkoin läpi ja rakennetaan mahdollisesti kutsutut
muut struct-näkymät, ja näkymä avataan näytölle
Edellä olevasta on poikkeus näkymä, joka on alinäkymä
varsinaiselle. Silloin .onAppear() saattaa jäädä suorittamatta tai
mahdollisella suorittamisella ei ole vaikutus sihhen mitä näytöllä
näkee. Mutta joissain rakenteissa kuitenkin toimii odotetulla
tavalla. Jos ei toimi hyvin, kaikki muuttujat pitää laittaa
kuntoon kutsuvassa näkymässä ja tuoda alinäkymään @Binding
-määritteinä.
Erilaista toimintalogiikkaa, kuten funktioita, voidaan kutsua
myös toimintokomentojen yhteydessä: buttonit,
alasvetovalikot (onChange), menut jne.
(23.3.2021)
Alertti eli käyttäjäilmoitus on ikkuna jossa näytetään varoitus
tai kysytään vaihtoehtoa miten jatketaan. Se toteutetaan
laittamalla ContentView:n alalaitaan modifioija .alert(..){} jonka
sisällä alertin sisältö. Tämä modifioija on sijoitettava
ContentView:lle. Nykyään joissain sovelluksissani käytän
monipuolisempaa alerttia, jossa on mukana yksinkertainen alertti
ja vaihtoehtobuttoneilla varustettu. Otin käyttöön tällaisen
alertin (kiitos stackoverflow, nimimerkki aslebedev):
struct
AlertItem: Identifiable
{
var id = UUID()
var otsa = Text("")
var sisus: Text?
var ekaBtn: Alert.Button?
var tokaBtn: Alert.Button?
}
Ja lähtönäkymän alareunassa minulla on modifioija:
.alert(item:
$tila.alertItem)
{
alertItem in
let ebtn = alertItem.ekaBtn
let tbtn = alertItem.tokaBtn
if ebtn == nil
{
return Alert(title: alertItem.otsa,
message: alertItem.sisus,
dismissButton: tbtn!)
}
else if tbtn == nil
{
return Alert(title: alertItem.otsa,
message: alertItem.sisus,
primaryButton:
ebtn!, secondaryButton: .default(Text("Ei")))
}
else
{
return Alert(title: alertItem.otsa,
message: alertItem.sisus,
primaryButton:
ebtn!, secondaryButton: tbtn!)
}
}
Muuttuja tila on apumuuttuja (@State), joka on input kaikille
näkymilleni. Sillä on komponentti alertItem, joka välittää tiedon
alertin tarpeesta. Kun esimerkiksi kolmas "päällä" oleva näkymä
haluaa alerttia, se saadaan avautumaan apumuuttujan kautta
kyseisen kolmannen näkymän päällä.
Alertti saadaan näkyviin esimerkiksi buttonista "Poistu" näin
(tässä tilanteessa käyttäjä ei ollut tallentanut):
virhez =
"Haluatko hylätä tekemäsi muutokset?\n\n(Kasvimuok C01-02)"
print(virhez)
tila.alertItem = AlertItem(
otsa: Text("Varmistus"),
sisus: Text(virhez),
ekaBtn: .default(Text("Kyllä"), action:
{ tila.idrivi =
EIOO
seurx.removeFirst()
} ),
tokaBtn: .default(Text("En"), action: {
} ))
Rivi seurx.removeFirst() aiheuttaa sen, että esillä oleva näkymä poistuu ja tilalle tulee pinossa seuraava (pinossa olevan tunnuksen mukaan piirretään ao. näkymä käyttäen muuttujan tila tilatietoja).
Käyttämäni apumuuttujat seurx ja tila, katso selitys
täältä: sovelluksen arkkitehtuuri.
(19.12.2021) - Taulukkoasiaa on omalla sivullaan täällä.
(23.10.2020)
SwiftUI:ssa on hyvin yksinkertaista koota ikkuna eri näkymäosista,
kuten yllä vaaleansininen osa, vihreä osa ja taulukko. Ne on usein
järkevää sijoittaa omiksi tiedostoikseen, joita sitten päätiedosto
kutsuu.
Ikkunan osat kootaan samalla periaatteella kuin C# WPF:ssä eli kootaan rinnakkain ja peräkkäin osia (HStack ja VStack) niiden kokoa määrittelemättä ja ne sitten vievät sen koon mitä yksittäiselementtien (tekstikenttä, checkbox jne) tila vaatii. Osille saa kyllä koonkin ilmoittaa, mutta usein se on tarpeen vain koko ikkunalle. Lisäksi voidaan määritellä mihin reunaan osat laitetaan, niiden välimatkoja ym, jos oletusarvot eivät kelpaa.
Tässä esimerkkinä näyttö, jossa muokkaan kasvin löytöpaikan
tietoja, ja voin samalla katsoa karttakuvaa todetakseni menivätkö
koordinaatit kohdalleen:

(5.4.2021)
Näytän eri sovelluksissani kolmenlaisia karttoja: Applen kartta,
OpenStreetMap ja Maanmittauslaitoksen maastokartta. Näitä varten
tarvitaan koordinaatit ja paikkakunnan nimi. Maastokartta haluaa
NorthEast-koordinaatit, muut haluavat LatiLongi-koordinaatit.
Koordinaattien muuntaminen LatiLongi -> NorthEast
ohjelmallisesti löytyy täältä.
Applen karttaa varten annan Tila-muuttujan parametreina
tarvittavat tiedot, muille annan valmiin url-merkkijonon:
WebView(filez: urliz) // Maastokartta, OpenStreetMap
MapView(tila:
$tila) // Applen kartta
Tällä tavoin esitetään maastokartan ja OpenStreetMapin urlit, ja
itse sovelluksessa olen hakenut koordinaatit tällaisiin
lausekkeisiin:

Tässä maastokartan ja OpenStreetMapin näyttäminen:
import
SwiftUI
import WebKit
// ************************************
struct WebView: NSViewRepresentable
{
let filez: String
func makeNSView(context: Context) -> WKWebView
{
return WKWebView()
}
func updateNSView(_ nsView: WKWebView, context:
Context)
{
let urlReq = URLRequest(url:
URL(string: filez)!)
nsView.load(urlReq)
}
}
Tässä Applen kartan näyttäminen:
import
SwiftUI
import MapKit
//
************************************
struct MapView: View
{
@Binding var tila: Tila // täältä
saadaan koordinaatit ja paikan nimi
@State private var region =
MKCoordinateRegion()
@State private var sities = [siti]()
var body: some View
{
// kartan zoomaus
näppäimillä + ja -
Map(coordinateRegion:
$region, interactionModes: MapInteractionModes.zoom,
annotationItems: sities)
{
siti
in
MapAnnotation(coordinate: siti.coordinate)
{
PlaceAnnotationView(title: siti.nimi)
}
}
.onAppear()
{
print("MapView2 MV01-01: latiz = \(tila.latiz)")
sities.append(.init(nimi: tila.kinfoz, coordinate:
.init(latitude: Double(tila.latiz) ?? 0.0, longitude:
Double(tila.longiz) ?? 0.0)))
region = MKCoordinateRegion(center:
CLLocationCoordinate2D(latitude: Double(tila.latiz) ?? 0.0,
longitude: Double(tila.longiz) ?? 0.0),
span: MKCoordinateSpan( latitudeDelta: 0.3,
longitudeDelta: 0.3) // kauempaa esim 10, 10
)
}
}
}
// ************************************
// sijaintipaikan tiedot
struct siti: Identifiable
{
let id = UUID()
let nimi: String
let coordinate: CLLocationCoordinate2D
}
// *************************************
// Paikan osoitin kartalla
struct PlaceAnnotationView: View
{
@State private var showTitle = true
let title: String
var body: some View
{
VStack(spacing: 0)
{
Text(title)
.font(.callout)
.padding(5)
.background(Color(.white))
.cornerRadius(10)
.opacity(showTitle ? 0 : 1)
Image(systemName: "arrowtriangle.up.fill")
.font(.caption)
.foregroundColor(.red)
.offset(x: 0, y: 5)
Image(systemName: "mappin.circle.fill")
.font(.title)
.foregroundColor(.red)
}
.onTapGesture
{
withAnimation(.easeInOut)
{
showTitle.toggle()
}
}
}
}
(5.4.2021)
Menuja voi koodata sovellukseen kahdella eri tavalla: ikkunan
yläreunaan, tai sovellusikkunan sisään.
Ikkunan yläreunaan saa omat menunsa oletuksena vakiomenujen View
(Näytä) ja Window (Ikkuna) väliin. Siihen käytetään
SwiftUI-lauseketta CommandMenu, joka on laitettava sovelluksen
avaustiedoston sisään, WindowGroup:n muuttajaksi .commands ja
netistä löytyy hyvin esimerkkejä. Voi käyttää vain, jos
sovelluksen lifecycle on "SwiftUI Lifecycle".
Itse mieluummin laitan menut sovelluksen pääikkunan kehysten
sisään, tässä menut Tilastot, Ylläpito ja Tietoja:

Menurivi koodataan HStack:n sisään kuten muutkin peruselementit
(23.10.2020)
Tässä matriisinäkymä, jossa yksi kuva valittuna:

Alla matriisinäkymän koodaus ilman yläreunan buttoneita. Yksittäisen kuvat ovat taulukossa 'kuvat', jonka alkiot ovat class Kuvainfo, jossa alkioina ovat mm. kasvin id tietokannassa, kasvin nimi ym. Koodin käyttämä taulukko on tarkastettava etukäteen ennen kutsua siten, että saatava kuva ei ole nil, muutoin sovellus kaatuu. Tai sitten koodiin on lisättävä if -lauseke siltä varalta (jota minä käytännössä käytän). Tässä käytetään käyttäjän omissa kansioissa olevia tiedostoja, joten Xcode:ssa on tämä mahdollistettava Entitlements-osiossa laittamalla SandBox pois päältä.
ScrollView
(31.5.2021)
Kuvan esittäminen erillisessä ikkunassa kuvaa koskevien tietojen
kanssa on erittäin yksinkertaista. Alla yksinkertaistettu
esimerkki kuvasta ja koodista. Vaikeammaksi koodaus muuttui, kun
halusin kuvaikkunaan lisää toiminnallisuuksia: a) näytä ikkunassa
kasvin kuva pikselitarkkana ilman pienennystä ja skrollit
ympärillä, b) siirrä skrollien avulla kuvan kohtaa joka näkyy
pikselitarkkana, c) pienennä kuvaa ikkunassa siten että se näkyy
kokonaan ilman skrolleja, d) terävöitä kuvaa. Tämä ei onnistunut
useissa tapauksissa, jos latasin kuvatiedoston imageksi kaikkialla
ehdotetulla tavalla:
ima =
NSImage(contentsOfFile: kpolkuz)
Image(nsImage: ima!)
Totesin, että NSImage piti rakentaa hakemalla suurinta kuvakokoa
vastaava BitmapImageRep ja lisäämällä se NSImageen.
Ensimmäinen tapa antaa kuvan mitoiksi esimerkiksi WxH = 944.64 x
628.66 kun taas jälkimmäinen antaa samalle kuvalle 3056 x 4592.
Kuvan koko on NSImage-rakenteessa siten, että lauseke Image(nsImage: ima!)
käyttää sitä, eikä ns. oikeita mittoja voi erikseen antaa.
Oheisena funktio, jolla Image voidaan hakea tiedostosta. Tästä on
riisuttu pois kaikki tarkistukset, jotka on syytä laittaa oikeaan
sovellukseen:
func
kokoaKuva2b(polkuz: String) -> NSImage?
{
let reptit =
NSBitmapImageRep.imageReps(withContentsOfFile: polkuz)!
let oli = reptit.count // oltava > 0
var w = 0
var h = 0
var ivali = 0 // suurinta kuvaa
edustava repti
// etsitään suurinta kuvaa vastaava imagerep
ja se otetaan
for i in 0..<oli
{
if reptit[i].pixelsWide
> w
{
w =
reptit[i].pixelsWide
h =
reptit[i].pixelsHigh
ivali =
i
}
}
let imarepuusi = reptit[ivali] //
laitetaan vain suurimman kuvan imagerep mukaan
let imax = NSImage.init(size: CGSize(width:
w, height: h))
imax.addRepresentation(imarepuusi)
return imax
}

ScrollView()
{
if ima != nil // koodissa onAppear-osa lataa
kuvan tiedostosta
{
Image(nsImage: ima!)
.resizable() //
kuva sovitetaan tarjotulle alueella
.aspectRatio(contentMode: .fit)
}
}
.frame(minWidth: 400, minHeight: 300)
.frame(maxWidth: .infinity)