Nummen kirkonkylä (Lohja)

  Pääsivulle  |  TIETOTEKNIIKAN PÄÄSIVULLE

Ohjelmointivinkkejä (macOS Swift)


SwiftUI

(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.


Swift 5

(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.

Seuraavassa on esitetty joitain ohjelmointivinkkejä asioista, joista useita sain itse jonkin aikaa selvitellä:

- Ikkunan avaamisen ohjelmointi (XIB:n kanssa)
- Kuva näytölle halutun kokoisena (XIB:n kanssa)
- Paneeli-ikkunan ohjelmointi (XIB:n kanssa):  ohjelmointiohjeita,   koodausesimerkki.
- Alasvetovalikko (XIB:n kanssa)  täällä.
- Taulukkonäkymän (XIB:n kanssa)  ohjelmointia on täällä ja lisäksi   Colleview-esimerkki
- Kuvamatriisi (XIB:n kanssa)  ohjeita täältä. Ja lisäksi Colleview-esimerkki
- Dynaaminen checkbox-ryhmä (XIB:n kanssa)   Oheisessa zip-paketissa on lähde- ja Xcode-tiedostot.
- Kartan näyttäminen, koordinaatit
- Koordinaattien keskinäisen etäisyyden laskeminen
- Ovatko koordinaatit Suomessa, koodiesimerkkejä
- Videoleikkeet
- Skandit macin tiedostonimissä
- File dialog (NSOpenPanel ja NSSavePanel)
- Exif-koodit
- Tiedoston päivämäärät ja koko
- Toisen sovelluksen käynnistäminen


IKKUNAN AVAAMISEN OHJELMOINTI (XIB:n kanssa)

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.

KUVA NÄYTÖLLE HALUTUN KOKOISENA (XIB:n kanssa)

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ä.

KUVAMATRIISI  (XIB)

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.

DYNAAMINEN CHECKBOX-RYHMÄ  (XIB)

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)
    }
}


KARTAN NÄYTTÄMINEN, KOORDINAATIT

Monissa sovelluksissani asioiden sijaintia näytetään kartalla. Esimerkiksi albumisovelluksessa kuvan ottopaikka ja herbaariosovelluksessa kasvin löytöpaikka. Näiden sovellusten tietokannoissa on EUREF-FIN -koordinaatit, jotka nykyään saa helpoiten suoraan kameran tai kännykän tekemästä kuva- tai videotiedostosta. Saahan ne myös GoogleMapsin näytöltä valitsemalla hiiren oikealla painikkeella 'Mitä täällä on'. Sovellukseni näyttävät sijainnin käyttäjän valinnan mukaan joko OpenStreetMapin karttapohjalla, Apple:n karttapohjalla tai Maanmittauslaitoksen maastokartalla.
 
Käytin aiemmin Googlen APIa (GoogleMap), mutta sitten Google vaati luottokortin numeroa ja suostumusta veloitukseen, vaikka tämäntyyppisestä käytöstä Google ei (toistaiseksi) laskuta. Luottamukseni ei ole kovin suurta siihen, etteikö siinäkin asiassa asiakkaalle ilmoittamatta muutettaisi sopimusehtoja.

Karttapiste on helpointa esittää urlin avulla, jossa on sisään leivottuna koordinaatit. OpenStreetMap haluaa koordinaatit LatiLongi, kun taas Maanmittauslaitoksen maastokartta haluaa koordinaatit muodossa NorthEast:
url
      kahdessa muodossa

Ota xib-ikkunassa käyttöön näyttöelementti WKWebView ja vie sinne edellä tehty html-koodin sisältävä tiedosto:
let urli = URL(string: openz)   // tai maasz
let requ = URLRequest(url: urli!)
webviewOma.load(requ)   // tämä on WKWebView

SwiftUI:ssa vastaav voidaan tehdä esimerkiksi oheisen näkymän avulla, jolle tuodaan edellämainittu openz tai maasz muuttujaksi urliz:
struct WebView: NSViewRepresentable
{
let urliz: String
func makeNSView(context: Context) -> WKWebView
{
return WKWebView()
}
func updateNSView(_ nsView: WKWebView, context: Context)
{
let urlReq = URLRequest(url: URL(string: urliz)!)
nsView.load(urlReq)
}
}

Huomaa, että WKWebView:n käyttäminen onnistuu vain, jos WebKit-kirjasto on linkitetty projektiin: Valitse target "MinunSovellukseniNimi", ja katso sieltä osio "Linked Frameworks and Libraries". Plus-merkillä lisää sinne WebKit.framework. Ilmankin sitä kaikki toimii, kun sovellusta ajaa Xcode:ssa, mutta app ei toimi ajettaessa sitä esimerkiksi Ohjelmat-kansiosta.


Koordinaattien keskinäisen etäisyyden laskeminen

Sovelluksessa voi olla tarpeen selvittää kuinka kaukana jokin paikka on toisesta paikasta, kun molempien koordinaatit tunnetaan. Minulla tämä tuli eteen, kun halusin löytää tietokannasta kuvat, joiden ottopaikat ovat tietyn ympyrän sisällä tietystä koordinaattipisteestä. Tietokannassa on kuvien koordinaatit. Seuraava funktio laskee onko kahden pisteen etäisyys alle vai yli annetun etäisyyden ja tarvittaessa myös pisteiden etäisyyden kilometreinä. Funktion sisällä on nopea karkealaskenta ja lisäksi noin 7 kertaa hitaampi tarkka laskenta:
// macOS 10.14  Swift 4
class func koordEtaisyys(_ dlat1: Double, dlon1: Double, latz2: String, lonz2: String, detmax: Double) -> Double
{
  let R = 6371000.0  // maapallon säde metrejä
  if (detmax > 1000000.0) || (detmax < 0.0 && detmax != -1.0) // yli 1000 km ei kelpaa
  {
    print("Meka M25-01: Ei kelpaa input detmax = \(detmax)")
    return -1.0
  }
  if latz2.isEmpty || lonz2.isEmpty
  {
    print("Meka M25-05: input tyhjä koord")
    return -1.0
  }
  let dlat2 = Double(latz2) ?? 0.0
  let dlon2 = Double(lonz2) ?? 0.0

  // Tarkastetaanko ovatko koordinaatit identtiset
  let ero1 = dlat1 - dlat2
  let ero2 = dlon1 - dlon2
  if ero1 < 0.0001 && ero1 > -0.0001 && ero2 < 0.0001 && ero2 > -0.0001
  {
    print("Meka M25-09: koordinaatit identtisiä")
    return 0.0
  }
  // Muunnetaan inputit radiaaneiksi
  let fii1 = (Double.pi / 180) * dlat1
  let fii2 = (Double.pi / 180) * dlat2

  if detmax > 0.0
  {
    let ddlat = abs(R * (fii2 - fii1)) // pikalaskenta leveyspiiriä pitkin
    if ddlat > detmax
    {
      return detmax + 1000.0         // liian kaukana toisistaan
    }
  }

  let lan1 = (Double.pi / 180) * dlon1
  let lan2 = (Double.pi / 180) * dlon2

  if detmax > 0.0
  {
    let ddlon = abs(R * (lan2 - lan1) * cos((fii1 + fii2) / 2)); // pikalaskenta pituuspiiriä pitkin
    if ddlon > detmax
    {
      return detmax + 1000.0                   // liian kaukana toisistaan
    }
  }

  // Lasketaan etäisyys tarkemmalla kaavalla
  let x = (lan2 - lan1) * cos((fii1 + fii2) / 2)
  let y = fii2 - fii1
  let dd = sqrt(x * x + y * y) * R
  return dd
}

Ovatko koordinaatit Suomessa, onko nettiyhteyttä

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ä.

VIDEOLEIKKEET

Tällaisilla Swift-koodiriveillä voi käyttää videoleikkeen katsomiseen esimerkiksi sovellusta mpv:
let polz = "/Applications/mpv.app/Contents/MacOS/mpv"
let argms = [
/Users/Erkki/Desktop/tilap/BigMama.flv]
let homma = Process()
homma.launcaPath = polz
homma.arguments = argms
homma.waitUntilExit()

Edellämainittu mpv on omien sovellusten lisäohjelmaksi hienosti sopiva, monipuolinen ja jatkuvasti ylläpidetty ilmaissovellus. Hakeudu netissä esimerkiksi seuraavaan osoitteeseen: https://github.com/mpv-player/mpv/releases

Videoleikkeistä tarvitaan hakutuloksiin ja metatiedon katselunäyttöihin yksittäinen kuva. Se otetaan videoleikkeen alusta esimerkiksi kolmen sekunnin päästä. Siihen käytetään sovellusta ffmpeg. Aina voi käyttää sovelluksen 32-bittistä versiota.

Sovelluksella ffmpeg on valtava määrä vaihtoehtoisia lippuja sen toiminnan määrittämiseksi. Alla oleva komento ottaa videopätkästä 120x90 -kokoisen kuvan kolmen sekunnin päästä videon alusta. Huomaa, että otettavan kuvan koon määreiden on oltava parillisia lukuja. Sovelluksessa tämä rivi tarkistuksineen on tietysti Swift-koodin sisällä.
ffmpeg -y -i video.mpg -an -ss 00:00:03 -s 120x90 -vframes 1 -f mjpeg  tuloskuva.jpg

Tässä esimerkki miltä koodi näyttää:
// vidz = videotiedoston polku
// polz = tuloksena olevan kuvan polku
// ffz = ffmpeg - tiedoston täysi polku
let argms = ["-y", "-i", vidz, "-an", "-ss",
"00:00:03", "-vframes", "1", "-f", "mjpeg", polz]
let task = Process.launchedProcess(launchPath: ffz, arguments: argms)
task.waitUntilExit()

SKANDIT MACIN TIEDOSTONIMISSÄ

Macissä sovellukset (kuten Finder) voivat tallentaa levylle tiedostonimiin skandeja omintakeisella tavalla, eli ne voivat olla koottu kahdesta perättäisestä merkistä (skalarista). Tämä voi tulla ongelmana vastaan tilanteessa, kun tiedostonimi on haettu Macissä tiedostojärjestelmästä (esim. NSOpenPanel, 'failidialogi') ja tallennettu sovelluskohtaiseen tietokantaan, ja sitten tietokantaa halutaan lukea Windows-koneella. Taustalla on se, että kun Macciin tallentaa jonkun tiedoston, niin tiedostonimen skandit voivat tallentua kahdessa osassa "a cluster of two scalars".  Jos tiedostonimiä sisältävää tietokantaa on tarkoitus käyttää myös Windowsissa, on tietokantaan vietävät skandit korjattava yksiosaisiksi. Tällaista skandia osaavat sekä Mac että Windows käsitellä oikein ja se tulee myös näytölle oikein. Windows ei tunnista tiedostoa, jos sen nimessä skandi on kaksiskalarisena.

Macissä yksiskalarinen ja kaksiskalarinen skandi merkkijonon osana on identtinen string-vertailussa, mutta Windowsissa ei ole.

Macin tiedostojärjestelmää käsittelevät kirjastot ja niiden funktiot tuovat skandit kaksiskalarisina tiedostojärjestelmästä, esimerkiksi
NSOpenPanel(..) ja FileManager-luokan contentsOfDirectory(..).

Minulla on useita sovelluksia, joihin liittyvää tietokantaa haluan käyttää sekä Macissä että Windows-koneessa.

Tarkastellaan tiedostonimiä Windowsissa:
Allaolevassa esimerkissä ylemmän polkunimen (Värikuvat_2016...) skandi on SQLite-kannassa Macin normaalikäytännön mukaisesti kaksiskalarisena, kun taas alemmassa esimerkissä se on yksiskalarisena. Alempi esimerkki on "normaali" Windowsin ymmärtämä skandi (jota sqlite3:n shell ei näytä "oikein"):
dos listaus

Alla näyttöjä testisovelluksestani Macissä. Yläreunassa on ensin failidialogista haettu tiedostonimi ja sen alla kirjain kerrallaan skalareina ("ascii-koodeina"). Esimerkiksi iso Ö muodostuu kahdesta skalarista 79 ja 776:
Skandit Å Ä Ö skalareina

Kokeilin vielä millaisia skandeja tekstikentistä noukitut ovat Macissä. Kuten kuvasta näkee,  tässä käyttötilanteessa skandit ovat Macin sisällä yksiskalarisia:
Å Ä Ö tekstikentästä

Tällaisia skandit ovat ovat yhtenä skalarina eli vanhat tutut ascii-koodit:

Å = "\u{C5}"  
Ä = "\u{C4}"
Ö = "\u{D6}"
å = "\u{E5}"
ä = "\u{E4}"
ö = "\u{F6}"


Alla koodiesimerkit (Swift 4) skalarien käsittelemiseksi:

//----------------------------------------------------------------------------
// Funktio, joka korjaa kaksiskalariset skandit yksiskalarisiksi
func korjaaSkandit(_ alkuz: String) -> String
{
  var korjaz = ""    // tähän kootaan korjattu tulos
  var x0: UInt32 = 0
  var x: UInt32 = 0
   
  for mki in alkuz
  {
    let s = String(mki).unicodeScalars
    let alku = s.startIndex
    var viiva = 0
    var i = alku
    while(true)
    {
      x0 = x
      x = s[i].value
      if viiva == 1    // nyt x0 on klusterin eka osa ja x toinen
      {
        if x0==65 && x==778    // Å
        {
          korjaz += "\u{C5}"
        }
        else if x0==65 && x==776    // Ä
        {
          korjaz += "\u{C4}"
        }
        else if x0==79 && x==776    // Ö
        {
          korjaz += "\u{D6}"
        }
        else if x0==97 && x==778    // å
        {
          korjaz += "\u{E5}"
        }
        else if x0==97 && x==776    // ä
        {
          korjaz += "\u{E4}"
        }
        else if x0==111 && x==776    // ö
        {
          korjaz += "\u{F6}"
        }
        else
        {
          korjaz += String(mki)
        }
      }
      viiva = viiva + 1
      i = s.index(after: i)
      if i == s.endIndex
      {
         break
      }
    }
    if viiva == 1
    {
      korjaz = korjaz + String(mki)
    }
  }
  return korjaz
}

//----------------------------------------------------------------------------
// Funktio, joka muuttaa merkkijonon toiseksi merkkijonoksi, jossa ovat skalariarvot
// esillä lukuarvoina (testisovellukseen)
func teeSkalariarvot(_ alkuz: String) -> String
{
  var tuloz = ""    // tähän kootaan skalariarvot
   
  for character in alkuz
  {
    let s = String(character).unicodeScalars
    let alku = s.startIndex
    var viiva = 0
    var i = alku
    while(true)
    {
      if viiva > 0
      {
        tuloz += "-"
      }
      let x = s[i].value
      tuloz += String(x)
      viiva = viiva + 1
      i = s.index(after: i)
      if i == s.endIndex
      {
         break
      }
    }
    tuloz += " "
  }
  return tuloz
}

FILE DIALOG (NSOpenPanel ja NSSavePanel)

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
       }
    }
}


EXIF-KOODIT

Exif-koodit kertovat yksityiskohtaisia tietoja valokuvasta eli kameralla tehdystä kuvatiedostosta. Netistä löytyy helposti tietoja mitä exif-koodeja ylipäätään on olemassa ja miten niitä saa ulos ohjelmallisesti.

Macissä on Swift-kielellä melko yksinkertaista hyödyntää kuvatiedostojen exif-koodeja. Alla on esimerkkikoodia muutamien exif-koodien noukkimisesta. Samalla periaatteella niitä saa noukittua lisääkin.

Jotkut exif-tiedot tiedoston sisällä ovat lukuja, toiset merkkijonoja tai bittijonoja joissa esimerkiksi tietyt kaksi merkkiä tarkoittavat jotain.

Tässä on Swift 5 -kielellä  koodiesimerkki miten exif-tietoja saa noukittua. Koodissa 'polkuz' on absoluuttinen polku kuvaan, jota tarkastellaan.

// Kurkistetaan levyllä kuvatiedoston metatietoja, eikä ladata kuvaa muistiin:
let cgref = CGImageSourceCreateWithURL(URL(fileURLWithPath: polkuz) as CFURL, nil)
if cgref == nil
{
   print("cgref = nil")
   return
}
       
let imaprop: NSDictionary = CGImageSourceCopyPropertiesAtIndex(cgref!, 0, nil)
if imaprop == nil
{
   print("imaprop = nil")
   return
}
      

let dicti = imaprop as! [AnyHashable: Any]
let w = dicti[kCGImagePropertyPixelWidth] as! Int
let h = dicti[kCGImagePropertyPixelHeight] as! Int

// Orientation kertoo onko kuva valmiiksi katseluasennossa, vai pitääkö
// esimerkiksi kiertää 90 astetta. Arvo 1 = valmiina oikein
let suunta = dicti[kCGImagePropertyOrientation] as! Int?
print("width = \(w), height = \(h)")
if suunta != nil
{
   print("suunta = \(suunta!)")
}
       
// Etsi kuvan ottamisajankohta
// Muoto on "YYYY:MM:DD HH:MM:SS" jossa aika on 24 tunnin muodossa,
// ja pvm ja aika on erotettu välilyönnillä (hex 20)
// Pituus on aina 20, mukaan lukien päätemerkki hex 0
// Jos pvm tai aika on tuntematon, numerot on korvattu välilyönnillä
let exif = dicti[kCGImagePropertyExifDictionary] as! NSDictionary?
if exif != nil
{
   // Ajankohta jolloin kuva oli otettu tai tallennettu
   let aika0z: String? = exif![kCGImagePropertyExifDateTimeOriginal] as! String?
   if aika0z != nil
   {
      print("DateTimeOriginal = \(aika0z!)")
   }
   // Ajankohta jolloin kuva oli digitoitu
   let aikaz: String? = exif![kCGImagePropertyExifDateTimeDigitized] as! String?
   if aikaz != nil
   {
      print("DateTimeDigitized = \(aikaz!)")
   }
}

// Kuvan ottopaikan koordinaatit kameran GPS:n perusteella
// Esimerkiksi Latitude = 64.1234 S tarkoittaa GoogleMapin Lati = -64.1234
let gps: NSDictionary? = dicti[kCGImagePropertyGPSDictionary] as! NSDictionary?
if gps != nil
{
   let latiz: Double? = gps![kCGImagePropertyGPSLatitude] as! Double? // asteet desimaalilukuina
   if latiz != nil
   {
      print("Latitude = \(latiz!)")
   }
   let latrefz: String? = gps![kCGImagePropertyGPSLatitudeRef] as! String?
   if latrefz != nil
   {
      print("LatitudeRef = \(latrefz!)")    // N tai S;  S -> latitude negatiiviseksi
   }
   let longiz: Double? = gps![kCGImagePropertyGPSLongitude] as! Double?
   if longiz != nil
   {
      print("Longitude = \(longiz!)")
   }
   let lonrefz: String? = gps![kCGImagePropertyGPSLongitudeRef] as! String?
   if lonrefz != nil
   {
      print("LongitudeRef = \(lonrefz!)")  // E tai W; W -> luku negatiiviseksi
   }
   let altitud: Double? = gps![kCGImagePropertyGPSAltitude] as! Double?
   if altitud != nil
   {
      print("altitude = \(altitud!)")  // korkeustaso merenpinnasta
   }
}

TIEDOSTON MUUTOSPVM, KOKO JA KUVANOTTOPVM

Tiedoston muutospvm ja koko:
let fm = FileManager()
do
{
   let attri: NSDictionary = try fm.attributesOfItem(atPath: polkuz) as NSDictionary
   let xx = attri.fileModificationDate()  // on NSDate, tiedoston muokkauspvm
   let xx2 = attri.fileCreationDate()    // tämä on 'node creation date'
   let df = DateFormatter()
   df.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
   let pvz = df.string(from: xx!)
   let xkoko = attri.fileSize()
   let ik = Int(xkoko / 1024)  // koko KB
   // tee jotain näillä: xx, xx2, pvz, ik
}
catch let error as NSError
{
   print("\(error.localizedDescription)")
}


Kuvatiedoston metatiedoista on usein tarpeen yrittää päätellä kuvauspäivämäärä. Edelläoleva fileModificationDate() on hyvä ehdokas, jos kuvaa ei ole muokattu ottamisen jälkeen. Se kertoo ajankohdan jolloin tiedoston sisältöä on viimeksi muutettu. Huomaa, että fileCreationDate() antaa ajankohdan jolloin tiedosto on muodostettu, eli esimerkiksi kopioinnin yhteydessä, jolloin se on myöhempi kuin fileModificationDate.

Exif-tiedoista löytyy kaksi hyvää päivämäärätietoa lisää kuvauspäivämäärän arvaamiseksi: DateTimeOriginal ja DateTimeDigitized. Näistä jälkimmäinen on varmempi, jos sitä vain löytyy kuvatiedoston metatiedoista. Lisäksi Exif-koodeista löytyy mm. koordinaatit. Exif-koodien hakeminen on esitetty edellisessä luvussa.

TOISEN SOVELLUKSEN KÄYNNISTÄMINEN

Swift-sovelluksesta voi käynnistää toisen sovelluksen erillisenä tehtävänä. Alla esimerkki miten käynnistetään videon katselu. Siinä tulee antaa sekä sovelluksen että videotiedoston koko polku:

let homma = Process()
homma.launchPath = "/Applications/VLC.app/Contents/MacOS/VLC"
homma.arguments = ["/users/erkki/desktop/BigMama.flv"]
homma.launch()
homma.waitUntilExit()


Ylläolevan vaihtoehto on esitetty alla. Tässä hyödynnetään Macciin asennettujen sovellusten luetteloa:
homma.launchPath = "/usr/bin/open"
homma.arguments = [polkuz, "-a", "vlc"]
homma.launch()
homma.waitUntilExit()


Tällainen toimii SwiftUI:ssa
let argut = ["a", "b"]
let task = Process.launchedProcess(launchPath: sovepolkuz, arguments: argut)
task.waitUntilExit()



--------------------------------
(sivua muokattu  4.4.2021)