(4.4.2021)
SwiftUI
on näytönkuvailukieli, joka toimii erinomaisesti yhdessä
Swift-kielellä tehtyjen funktioiden kanssa. Enää ei tarvita
erillistä graafisen ulkoasusuunnittelun näyttöä, joka tekee
xib-tiedoston. Tämä työkalu oli vuosien varrella mutkistunut
hallinnoimaan erilaisia sovelluksen logiikan muuttujia, ja sen
uusin versio oli epäkäytännöllinen vähänkään isommalle
projektille, jossa on paljon eri ikkunoita.
Eli nyt Macissä näyttöjen rakentaminen on yhtä näppärää ja nopeaa
kuin on ollut monta vuotta Windows-ympäristön Visual Studiossa
(WPF-tekniikka).
Keskeneräinen SwiftUI on kuitenkin ainakin macOS-ohjelmoijalle.
Esimerkiksi monisarakkeisen taulukon esittäminen on hankalaa,
samoin menujen ohjelmointi ja netistä löytyvät ohjeet ja neuvot
ovat pääasiassa iOS-ohjelmoijille. Ihan onnistuneita sovelluksia
olen saanut ongelmista huolimatta aikaiseksi kiertämällä
puutteita.
Seuraavassa käsitellään Swift-ohjelmointia, kun näytöt ja niiden
välinen logiikka on rakennettu xib-työkalulla. Useat osiot
kelpaavat sellaisenaan myös SwiftUI-ympäristöön esimerkiksi
funktioiksi.
Täältä löytyy vinkkejä Xcode:n käyttämiseen.
(3.4.2021)
Swift on vuonna 2014 julkistettu Applen ohjelmointikieli
käyttöjärjestelmille iOS ja macOS tehtäviä ohjelmia varten. Kielen
uusin versio 5.0 on julkaistu loppusyksynä 2018 ja se toimii
parhaiten käyttöjärjestelmässä macOS 10.14 (Mojave) ja uudempi.
Versio 5.0 ei ole uusin, ja työn alla parhaillaan on 5.4.
Kuitenkin kaikki 5-alkuiset versiot sisältävät keskeiset toiminnot
samanlaisina.
Ohjelmointityökaluna ilmainen Xcode on erinomainen työkalu.
Kielen rakenne on itseään kontrolloiva ja Xcoden koodin tarkastaja
(SourceKit Service) ilmoittaa nopeasti potentiaalisista
ongelmista. Ohjelmointi on luontevaa, joustavaa ja hauskaa.
Pitkänkin koodilistauksen lukeminen on helppoa, varsinkin jos
kommentoinnista on huolehdittu. Täältä löytyy
vinkkejä Xcode:n käyttämiseen.
Swift-kieleen kuuluu melko pieni määrä avainsanoja, tyyppejä ja
perusrakenteita. Sen jatkona käytetään Applen Foundation-, Cocoa-
ja AppKit-kirjastoja, tai pelkkä import SwiftUI jos
näytön kuvailuun käytetään SwiftUI:ta.
Swift-kielellä tehtävän sovelluksen perusrakenteet ovat tuttuja
kaikille, jotka ovat ohjelmoineet C#:lla ja Javalla, eli käytössä
ovat luokat, funktiot ja monet muut nykyaikaisen kielen
perusasiat. Swiftissä ei tarvitse huolehtia muistin hallinnasta,
vaan esimerkiksi muuttujien ja luokkien varaama tila ohjelman
suorituksen edetessä tulee automaattisesti vapautettua.
Tässä koodausohjeita. Meillä on avattuna ikkuna (luokka
Alkuikku), jossa on painike, jota klikkaamalla pitäisi avautua
toinen ikkuna (luokka Kasvihaku) niin että vanha jää taustalle.
Molemmat ikkunat on siis ohjelmoitu omaksi luokakseen.
Luokkaan Alkuikku laita luokkamuuttujaksi:
var kasvihaku:
Kasvihaku?
Painikkeen käsittelyfunktiossa uusi ikkuna avataan näin:
kasvihaku =
Kasvihaku(windowNibName: NSNib.Name(rawValue: "Kasvihaku")
kasvihaku!.idkasz = z2 // annetaan luokalle
inputtia
let w = kasvihaku!.window // ikkuna avataan
näkyviin
w!.makeKeyAndOrderFront(nil)
Avautuva ikkuna (kasvihaku) elää omaa elämäänsä riippumatta
lähtöikkunasta (Alkuikku), joten sieltä ei voi saada dataa kun
käyttäjä sulkee sen. Jos sellaiseen on tarvetta, käytä modaalista
paneelia, katso jäljempänä.
Jos luokkamuuttujaa ei käytä, niin uusi ikkuna korvaa vanhan.
Ikkunan otsake
window.title =
"Otsake"
Nappaa kiinni jos käyttäjä poistuu punaisesta ruksista:
func
windowShouldClose(_: AnyObject!) -> Bool
{
print("windowShouldClose")
let c = teejotain() // tehdään
jotain lopputoimenpiteitä
return (c) ? true : false // true =
suljetaan, false = ei suljeta
}
Tee siivousta juuri ennen kuin sovellus suljetaan. Tämän paikka on
tiedostossa AppDelegate.swift:
func
applicationWillTerminate(_ aNotification: NSNotification)
{
// Tänne siivousta, esimerkiksi tallennusta
NSApplication.shared.terminate(self)
}
Sovellus lopettaa itsensä kun viimeinen ikkuna suljetaan. Tämän
paikka on tiedostossa AppDelegate.swift:
func
applicationShouldTerminateAfterLastWindowClosed(_ sender:
NSApplication ) -> Bool
{
return true
}
Jos haluat lopettaa sovelluksen heti esimerkiksi jonkin kohdatun
virheen takia, niin se käy funktiolla:
exit(0)
Muitakin tapoja ikkunan avaamiseen löytyy nopeasti, kun netistä
etsii vinkkejä tai yrittää ymmärtää Applen ohjeistusta. Edellä
esitetty on toiminut hyvin omissa sovelluksissani.
ImageWell on käytännössä hyvin toimiva ja taustalla valmiiksi
koodattu kontrolli kuvan näyttämiseen. Ilman lisäkoodausta kuva
täyttää kontrollin säilyttäen kuvan sivusuhteen.
Käyttöliittymäeditorissa raahaa ikkunaan kontrolli nimeltä
ImageWell ja tee siitä haluamasi kokoinen. Jos haluat, että
kontrolli muuttaa kokoaan, kun ikkunan kokoa muutetaan, niin älä
laita editorissa kontrollia aivan ikkunan reunoihin asti. Tämän
jälkeen sido constrainteilla kontrollin koko ikkunan kokoon:
ctrl-raahataan viiva ImageWellistä ikkunan reunan lähelle, kaikki
neljä reunaa.
Tee käyttöliittymäeditorissa ImageWell:lle outlet, jonka nimi alla
olevassa koodipätkässä on imawellOma. Kuvan saat paikoilleen näin:
if let kuva =
NSImage(contentsOfFile: "testikuva.png")
{
imawellOma.image = kuva
}
else
{
// ei onnistunut
}
NSView:n ja käyttöliittymäeditorin kontrollin "Custom View" avulla
voi saada saman aikaan, mutta mutkikkaammin. Tätä kautta saa
toteutettua kuvan esittämisen täysin haluamallaan tavalla.
Xcoden kontrolli ImageView on hyödyllinen, jos haluat laittaa
kuvan skrollausnäkymään tai itse vaihtaa kuvan zuumausta.
Tällä tavalla laitetaan NSImageView scrollien sisään ja vielä
seuraamaan ikkunan kokoa:
1. Lisää ikkunaan ImageView ja tee sille outlet
2. Valitse ImageView ja valitse menusta "Editor / Embed in
ScrollView"
3. Tee scrollview:lle constraintit ikkunan laitoihin, jos haluat
skrolli-ikkunan seuraavan ikkunan kokoa
4. Koodissa luokan määrittelyyn tulee lisätä delegaatti
NSWindowsDelegate
Ikkunan kokoa seuraavan kuvan tiedot pitää noukkia käyttäen
observeria
NotificationCenter.default.addObserver(self,
selector: #selector(NSWindowDelegate.windowDidResize(_:)), name:
NSWindow.didResizeNotification, object: nil)
Lausekkessa oleva windowDidResize() on funktio, joka ajetaan koon
muuttuessa, ja kuvan haluttu koko saadaan esimerkiksi:
let ww =
(self.window?.frame.width)! - 48.0
let hh = (self.window?.frame.height)! - 105.0
Sitten tee sovelluksessa kuvasta kooltaan muuttuneen ikkunan
kokoinen versio. NSImageView:lle laitettava kuva määritellään alla
olevalla tavalla. Tämä toimii myös jos kuva on aluettaan isompi ja
scrolli-ikkunan sisällä:
imageviewOma.setFrameSize(uuskuva!.size)
imageviewOma.image = uuskuva!
Edellä koodipätkässä käytetty NSImage(contentsOfFile: polkuz) ei
tuo aina oikeaa kuvaa, varsinkaan jos tiedostossa on kuva useassa
eri koossa, kuten tilanne monesti on varsinkin tiff- ja
jpg-kuvissa. NSImageView:n omat funktiot osaavat kyllä
muuten jotenkuten skaalata kuvia kontrollin käytettävissä olevaan
kokoon, mutta usein kannattaa tehdä skaalaus itse ja tarjota
NSImageView:lle valmiiksi oikean kokoinen kuva. Erityisesti tämä
on tärkeää, kun esittää kuvia NSCollectionView:ssä.
Täältä löytyvät
itse tehdyt funktiot, jotka antavat laadukkaan kuvan näytölle.
Jos haluaa tehdä kuvatiedostosta pienikokoisen laadukkaan
thumbnailin tallennettavaksi
tietokantaan, tarvitaan sovellukseen seuraavat olennaiset rivit:
let ima =
kokoaKuva6(tayspolkuz, blobx: nil, wid: 102, hei: 80)
let blob = ima!.tiffRepresentation
let blob2 = NSBitmapImageRep(data: blob!)!.representation(using:
.jpeg, properties:
[NSBitmapImageRep.PropertyKey.compressionFactor: 0.6])
Lopuksi blob2 viedään tietokantaan. Pakkausarvo on edellä 0.6
mutta tuloksena on kuitenkin varsin laadukas pikkukuva kooltaan
3-4 kiloa. Jos tietokannan koko ei ole kriittinen, arvo voi hyvin
olla 1.0, jolloin tilaa tarvitaan 10-12 kiloa. Minä käytän samaa
sqlite-tietokantaa sekä Macissä että Windows-koneissa, joten
kantaan on laitettava molemmille kelpaava kuvatiedosto eikä
esimerkiksi joitain vain Macin ymmärtämiä bitmäppejä.
Kuvajoukon esittäminen näytöllä on monessa tapauksessa kätevää,
jos kuvista halutaan valita joku esimerkiksi kuvan tai sen
tietojen esittämistä varten. Siihen voidaan käyttää
näyttöelementtiä NSCollectionView. Tässä esitettävät
yksittäiskuvat (-osat) ovat elementtejä NSCollectionViewItem. On
valitettavaa, että NSCollectionViewItem:n omat funktiot eivät osaa
skaalata pikkukuvaa terävänä, vaan siihen on tehtävä itse paremmat
funktiot.
Luokassa (ikkunassa), jossa kuvamatriisi esitetään, tulee
käyttöön delegate-funktioita, joita on pakollisia ja lisäksi
tarpeen mukaan käytettäviä. Yksinkertaisinta on viitata
delegaatteihin luokan määrittelyrivillä, esimerkiksi:
class Hakutulos3: NSWindowController, NSCollectionViewDataSource,
NSCollectionViewDelegate
Itse delegaattifunktiot kirjoitetaan luokan muiden funktioiden
sekaan. Niiden määrittelyn on oltava tarkasti delegaatin mukainen,
mutta sisällössä käytetään omia tietorakenteitasi tarvittavassa
laajuudessa.
Seuraavassa aineistoa kuvamatriisin rakentamiseksi:
CollectionView:n käyttöliittymän rakentamista ja sovelluksen
koodaamista varten löytyy ohjeita täältä.
Valmis koodausesimerkki (zip), avaa se Xcode:ssa: Colleview-esimerkki
Jotta esimerkki toimisi, korvaa koodissa (Tabikku.swift) oleva
kansiopolku .... Desktop/tilap/työ itsellesi paremmin
sopivalla ja sijoita sinne kuvia, joiden nimi on nyt koodissa
1.png, 2.png, ..., 12.png. Pääikkunassa on painike
"Yhdistelmäikkunaan", joka avaa uuden ikkunan (luokka
Tabikku), jossa on varsinainen NSCollectionView. Sen lisäksi
ikkunassa on NSTableView, jossa näkyy samaa dataa. Koodi
esittelee, miten valinnat toimivat rinnan taulukko- ja
matriisinäkymässä. Koodi antaa testitulosteita Xcode:n Debugger
Output -ikkunaan.
Jos checkboxien lukumäärä ei ole vakio, vaan tilanteesta riippuen
niitä voi olla esim. 0 - 60, niin ne on kätevintä perustaa
dynaamisesti tarpeen mukaan. Jos tila on rajattu, niin checkboxit
tulisi sijoittaa scrollview:n sisälle. Yritin ensin etsiä
ratkaisua käyttäen kontrolleja NSTableView ja NSCollectionView,
mutta koodiin jäi ongelmia ja toteutukset tulivat mutkikkaiksi.
Helpoin ja lopulta kätevin ratkaisu oli yksinkertainen:
ScrollView:n sisällä oleva alue NSView.
Oheisessa esimerkkisovelluksessa (Swift 4, Xcode 9.2) checkboxit
lisätään addSubview-funktiolla, ja näkymän (View) y-koordinaatit
on käännetty alkamaan yläreunasta. Kääntäminen on tehty käyttäen
apuluokkaa FlippedView, joka on kuten View, mutta luokkamuuttuja
isFlipped antaa true.
Lisäksi luokassa FlippedView on funktio changeBackroundColor,
jonka avulla checkbox-alueen
väri on helppo asettaa halutuksi, ja esimerkkikoodi käyttää sitä.
Oheisessa zip-paketissa on lähde- ja
Xcode-tiedostot. Lähdekoodissa on melko perusteelliset selitykset.
Luokka FlippedView on esitetty tässä:
//
***************************************
// 2.4.2018
// y-koordinaatit normaalisti lasketaan View:n alareunasta
// Muutetaan se laskemaan yläreunasta.
// Tässä myös taustavärin asettamismahdollisuus
class FlippedView: NSView
{
var backgroundColor = NSColor()
override var isFlipped:Bool
{
get
{
return true
}
}
override func draw(_ rect:NSRect)
{
super.draw(rect)
backgroundColor.set()
bounds.fill()
}
// taustavärin asettaminen
func changeBackgroundColor(color: NSColor)
{
backgroundColor =
color
setNeedsDisplay(self.bounds)
}
}

Täältä löytyy
koodiesimerkkejä siitä, miten voidaan katsoa ovatko koordinaatit
Suomessa, ja myös nettiyhteyden ohjelmallinen tarkistaminen.
Suomi-koordinaattien selvittäminen lähtee siitä, että
mahdollisimman harvoin jouduttaisiin hakemaan tietoa netistä.
ffmpeg -y -i video.mpg -an -ss 00:00:03 -s 120x90 -vframes 1
-f mjpeg tuloskuva.jpg "00:00:03", "-vframes",
"1", "-f", "mjpeg", polz]

Sovelluksessa tarvitaan mahdollisuutta valita tiedosto levyltä.
Tähän käytetään luokkaa nimeltä NSOpenPanel. Alla
koodausesimerkki. Huomaa, että NSOpenPanel antaa tiedostonimien
skandit kaksiskalaarisina, lisätietoja blogiosiossa 'Tiedostonimen
skandit'. Saatat tarvita luokan alkuun import AppKit.
var polkuz
= "" // tähän
lopputulos
let alkuz = "/Users/" + NSUserName() + "/Desktop"
// lähtökansio
let openDlg = NSOpenPanel()
openDlg.canChooseFiles = true
openDlg.canChooseDirectories = false
openDlg.allowsMultipleSelection = false
openDlg.message
= "Valitse kuva"
// Title ei ole enää käytettävissä
openDlg.prompt = "Valitse"
openDlg.directoryURL = URL(fileURLWithPath: alkuz)
let fileTypesArray = ["gif", "png", "jpg", "bmp"] // ei
tarvitse olla erikseen suuraakkosilla
openDlg.allowedFileTypes = fileTypesArray
if openDlg.runModal() == NSApplication.ModalResponse.OK
{
let valittu: [URL] = openDlg.urls
let purl = valittu[0]
let polz = purl.path
polkuz = korjaaSkandit(polz) // korjattiin
yksiskalaarisiksi, ks edellinen luku
}
Seuraavassa esimerkissä halutaan tallentaa tiedosto levylle, ja
käyttäjä valitsee kansion ja tiedostonimen. Käytetään luokkaa
NSSavePanel:
import
AppKit
// oletuskansioksi asetetaan käyttäjän työpöytä
let polkutyo = "/Users/" + NSUserName() + "/Desktop"
let tallPanel = NSSavePanel()
tallPanel.message = "Tallenna luettelo" // Title ei ole
enää käytettävissä
tallPanel.prompt = "Tallenna"
tallPanel.nameFieldLabel = "Nimellä:"
// Where-labeliä (kansionimi) ei voi vaihtaa
tallPanel.directoryURL = URL(fileURLWithPath: polkutyo)
tallPanel.begin
{ (result) -> Void in
if result == NSApplication.ModalResponse.OK
{
let valittu = tallPanel.url
let polkuz = valittu!.path
// tässä tallennetaan
tekstitiedosto
do
{
try
tuloz.write(toFile: polkuz, atomically: false, encoding:
String.Encoding.utf8)
}
catch _
{
//
epäonnistui
}
}
}