Det är ganska enkelt att koppla på loggning av obj-C-meddelanden. Värre är det att inte drunkna i mängden…
Tips: Logga Obj-C meddelanden
Ola Bäckström 10 juli 2008
Om du kör ett program från terminalen så kan du koppla på den inbyggda meddelandeloggningen.
Om du kör bash
, vilket är troligt, slå:
export NSObjCMessageLoggingEnabled=YES
och sedan startar du ditt Obj-C program från samma terminalfönster. Använd ett annat terminalfönster och slå ps
för att få reda på process-id (PID
) till programmet. Nu finner du en logfil under /tmp/
som kallas msgSends-XXX
, där XXX ska motsvara ditt program.
För att tolka de tre fälten, spana här. Notera att det bara är namnet på metoden (selector
n) som visas, inte ev. argument.
Jag har använt ovanstående för att gräva i odokumenterade AppKit-klasser. Tyvärr är datamängden stor (=enorm), använder en editor med sökning för att hoppa mellan relevanta rader.
_NSBindingAdaptor: Under ytan ... del 2
[editerad 081023, hade missat vad ba
var satt till]
Fortsättning på del 1.
Resan fortsätter in i en av AppKits hemliga klasser. Hittar vi något?
Ola Bäckström 9 juli 2008
(för stämningens skull, lägg till musik från Doktor Drøvels hemlighet)
Vad kan vi finna ut om _NSBindingAdapter
?
Jo, vår fina Nu är en lispinterpreter som ligger alldeles ovanpå Obj-Cns runtime . Vi kan lätt lista vilken klass som adaptern ärver från:
%(set ba (y _bindingAdaptor))
<_NSBindingAdaptor:34c590>
%
% (ba superclass)
NSObject
%
ba
sätts alltså till y
:s _NSBindingAdapter
. ba
är alltså ett ganska enkelt objekt… som speciellt inte ärver från NSBinder
som vi tidigare funderat runt.
Nu introducerar egna klasser (t ex NuCell
som används för att bygga listor på klassiskt lispmanér) men Nu-språket lägger också till en del finnesser på existerande AppKit klasser. Speciellt på NSObject
finns en del nya grejer. De två vi bryr oss om är:
+ (NSArray *) instanceMethodNames
+ (NSArray *) instanceVariableNames
Helt fantastiska funktioner för introspektion… Vi kan be en klass att redogöra vilka instansmetoder och instansvariabler som objekt av den klassen kan agera på.
(För att det verkligen ska gå in så repeterar jag) … vi kan alltså få reda på vilka instansvariabler vilken klass som helst lägger till – även Apples hemliga AppKitklasser.
Adapterns metoder
Det vi vill undersöka är _NSBindingAdapter
, så vi slår
% ((ba instanceMethodNames) list)
("_discardEditingForAllBinders"
"_editor:didChangeEditingState:bindingAdaptor:"
"_handleValidationError:description:inEditor:errorUserInterfaceHandled:bindingAdaptor:"
"_objectDidTriggerAction:bindingAdaptor:"
"_validateAndCommitValueInEditor:editingIsEnding:errorUserInterfaceHandled:bindingAdaptor:"
"addBinder:"
"binders"
"boundOutlineView:isItemExpandable:"
"browser:createRowsForColumn:inMatrix:"
"browser:selectRow:inColumn:"
"browser:willDisplayCell:atRow:column:"
"collectionView:didChangeToSelectionIndexes:"
"contentBinder"
"controller:didChangeToFilterPredicate:"
"controller:didChangeToSelectionIndexPaths:"
"controller:didChangeToSelectionIndexes:"
"controller:didChangeToSortDescriptors:"
"dealloc"
"defaultSortDescriptorPrototypeForTableColumn:"
"drawer:didChangeToState:"
"editableBinder"
"editorDidBeginEditing:"
"editorDidEndEditing:"
"enabledStateForMenuItem:"
"finalize"
"handleValidationError:description:inEditor:errorUserInterfaceHandled:"
"init"
"numberOfRowsInTableView:"
"objectDidTriggerAction:"
"objectDidTriggerDoubleClickAction:"
"outlineColumn:willDisplayCell:row:"
"outlineColumn:willDisplayOutlineCell:row:"
"outlineView:child:ofItem:"
"outlineView:didExpandItem:"
"outlineView:numberOfChildrenOfItem:"
"outlineView:willCollapseItem:"
"outlineView:willDisplayCell:forTableColumn:row:"
"outlineView:willDisplayOutlineCell:forTableColumn:row:"
"referenceBinder"
"removeBinder:"
"searchFieldCellOrControlDidClearRecents:"
"searchMenuTemplate"
"selectionMechanismWasDismissed:"
"tabView:didSelectTabViewItem:"
"tableColumn:didChangeToWidth:"
"tableColumn:willDisplayCell:row:"
"tableView:didChangeToSelectedRowIndexes:"
"tableView:didChangeToSortDescriptors:"
"tableView:shouldEditTableColumn:row:"
"tableView:updateVisibleRowInformation:"
"tableView:willDisplayCell:forTableColumn:row:"
"updateInvalidatedFont:forObject:"
"updateInvalidatedObjectValue:forObject:"
"updateInvalidatedTextColor:forObject:"
"validateAndCommitValueInEditor:editingIsEnding:errorUserInterfaceHandled:"
"window:didChangeToVisibleState:"
"windowDidResize:")
%
Ojojoj. (För att nush
ska skriva ut resultatet fint har vi förvandlat returvärdet från typen NSArray
till en lista.)
Det som, för mig, speciellt sticker ut är
tableColumn:willDisplayCell:row:
tableView:didChangeToSelectedRowIndexes: tableView:didChangeToSortDescriptors:
tableView:shouldEditTableColumn:row:
tableView:updateVisibleRowInformation:
tableView:willDisplayCell:forTableColumn:row:
Som verkar indikera på att _NSBindingAdaptor
kan agera som NSTableView
s delegat, och på så sätt förse “bindningsmekaniken” med information om vad själva tabellkontrollen (vyn i MVC) håller på med. Vidare…
"editorDidBeginEditing:"
"editorDidEndEditing:"
verkar indikera på att _NSBindingAdaptor
kan ta en roll i koordination av vyer som editerar samma objekt, (se NSEditorRegistration). Notera den lilla skillnaden att metoderna ovan börjar med editorDid...
medan de i det publicerade NSEditorRegistration
-protokollet börjar på objectDid...
.
Adapterns instansvariabler
Nu slår vi
% ((ba instanceVariableNames) list)
("_binders" "_contentBinder" "_editableBinder" "_referenceBinder")
%
Aha… till varje bundet objekt (i vårt fall y
) krokas en bindningsadapter på som i sin tur håller ordning på 4 saker. Getters finns, men setters är inte helt lätt att finna ur den långa listan ovan:
_binders
hör antagligen ihop med getterbinders
och metodenaddBinder:
._contentBinder
hör nog ihop med getterncontentBinder
._editableBinder
hör nog ihop med getterneditableBinder
_referenceBinder
hör nog ihop med getternreferenceBinder
Cirkeln sluts
I vårt fall, med objektet y
bundit mot x
, så är alla de tre sista tomma (nil
) men den första pekar på ett nytt odokumenterat objekt, på adressen 0x3655f0
:
% ((ba binders) list)
(<NSObjectParameterBinder: 0x3655f0>{object: <TTCrap: 0x3435f0>, bindings: attr=val})
%
% (set binderOne ((ba binders) objectAtIndex:0))
<NSObjectParameterBinder:3655f0>
% (binderOne description)
"<NSObjectParameterBinder: 0x3655f0>{object: <TTCrap: 0x3435f0>, bindings: attr=val}"
% (binderOne superclass)
NSBinder
%
En binder av typen NSObjectParameterBinder
håller reda på att vi har knutit ihop attributen attr
och val
. Detta är en av de 13 vi tidigare sett via F-scripts klassbrowser
Arbetshypotes
Jag tror att varje objekt som binds mot ett annat får en bindningsadaptor tillkopplad. Sedan, för varje bindning som görs läggs ett binder-objekt (subklassad från NSBinder
) till i listan ovan. För objekt som NSTableView
som har många bindingsmöjligheter (content
, isEditable
etc) kan det alltså kopplas till många NSBinder
s. Just nu ser min skiss ut så här:
Det är osäkerheter i bilden ovan.
Nu stoppar vi här. Andas. Nu ska jag dyka in i vad dessa NSBinder
s gör! Kanske en senare artikel.
_NSBindingAdaptor: Under ytan i binderträsket, del 1
[editerad 081023, förtydligat exemplet som visar att dataflöde baklänges inte fungerar]
Jag har inte kunnat lämna den odokumenterade NSBinderklassen och klassklustret runt om denna, utan sökt vidare. I denna artikel spanar vi speciellt på _NSBindingAdaptor
. I analysen används fantastiska Nu . Lisp & Cocoa i ett…
Ola Bäckström 9 juli 2008
F-script är ju väldigt polerat och lättillgängligt – bara att hämta binären och köra. Nu däremot kräver att man hämtar hem och bygger själv. Tröskeln känns högre… men när man väl lyckats med det och startar nush
(den fristående Nu-tolken) skingras mörkret. En välbryggd Cocoa-Lispinterpreter står beredd att hjälpa oss i ned under ytan, bortanför Apple-dokumenterat område.
Ånej ännu ett språk?
Har man den minsta lispbakgrund och god kännedom om Obj-C klarar man snabbt av den nya syntaxen. Här är ett exempel:
(class TTWrap is NSObject (ivar (id) val) (ivar-accessors))
(class TTCrap is NSObject (ivar (id) attr) (ivar-accessors))
Koden ovan skapar två nya klasser TTWrap
och TTCrap
med varsin attribut (instansvariabel), kallad val
och attr
. Eftersom vi ber snällt att få accessorer fixade är vi klara. Enklare än Obj-C!
Sedan behöver vi två instanser, en av varje klass:
(set x (TTWrap new))
(set y (TTCrap new))
Varje rad kan jämföras med x=[TTWrap new];
i Obj-C, där x
är en global variabel av typen id
.
Sedan sätter vi attributen via de automatgenererade setterna:
(x setVal:"apa")
(y setAttr:"traktor")
detta är liknar ju vanlig Obj-C något oerhört. För att dubbelkolla att vi gör rätt printar vi ut värdet av y:
% y
<TTCrap:3435f0>
%
(procenttecknet är nushprompten)
Öhh? jaja… vi fick ut att y
ligger på addressen 0x3435f0
och är av klassen TTCrap
. Bra, men värdet på attributen?
% (y attr)
apa
%
Finfint! Nu kan vi gå vidare.
Hur kommer _NSBinderAdapter
in i bildern?
Nu när vi har två objekt kan vi binda ihop dem:
(y bind:"attr" toObject:x withKeyPath:"val" options:nil)
Vanlig binding alltså. Vi knyter ihop x.val
med y.attr
. Vi kollar att bindningen fungerar
% (y attr)
apa
% (x setVal:"gorilla")
% (y attr)
gorilla
%
y.attr
har plötsligt fått samma värde som x.val
, och vidare uppdateras y.attr
av nya värden på x.val
. Precis som det ska.
Dataflöde baklänges fungerar inte:
% (y setAttr:"nytt")
% (y attr)
nytt
% (x val)
"gorilla"
%
Denna enkelrättade bindning gäller för oss när vi bundit två attribut med AppKit standard-bindningar.
Man nu börjar äventyret. Slå
% (y _bindingAdaptor)
<_NSBindingAdaptor:34c590>
%
Va? har y
en bindnings-vadå? Har x
det också?
% (x _bindingAdaptor)
()
%
Nej (en tom lista är lisps/Nus sätt att säga nil).
Vad som skett är att y
fått en bindningsadapter. En spännande grej, vi ska spana mer på den, men först ska vi bara kolla upp KVO-kunnskapen. (Du minns väl att KVB bygger ovanpå KVO?)
Finns KVO-proxyobjekt på riktigt?
Ett proxyobjekt borde ha poppat in och ersatt det observerade objektet, x
alltså:
% x
<NSKVONotifying_TTWrap:34c870>
% y
<TTCrap:3435f0>
%
Riktigt! Via isa-swappning har x
plötsligt blivit av klassen NSKVONotifying_TTWrap
. Spännande, och intressant är att ber man detta objekt om vilken klass den är av:
% (x class)
TTWrap
%
Illusionen är total: proxyklassen skickar vidare nästan allt till den vanliga TTWrap
klassen.
Mysteriösa NSBinder
I mitt grävande i Cocotron hittade jag referenser till några intressanta klasser, där alla ärver från _NSBinder . Det visar sig att i vanliga Apple-AppKit finns en klass kallad NSBinder som är odokumenterad. Vad är detta?
Ola Bäckström 25 juni 2008
Bakgrund
Den här korta artikeln skrapar lite på ytan runt NSBinder. Det kan vara intressant när man vill göra egna GUI-kontroller som är “bindings-enablade”. Saken är den att typiska operationer man skulle vilja göra för att hålla sitt UI-objekt synkat både upp- och nedströms längs knytningen är implementerade i NSBinder (och dess olika subtyper).
“Fyndet”
Friskrivning: naturligtvis är inte jag den förste som funnit NSBinder. Men det verkar vara få (=0?) direkta referenser med information på nätet.
Själva upptakten till fyndet var som sagt grävandet i Cocotron. Hittade dessa 3 klasser
_NSBinder
_NSKVOBinder
_NSMultipleValueBinder
Dessa skapas och används i kontroller för att sköta datatransporten som är involverad i en “binding”. Både i den riktning man automatiskt får när man anropar “bind:toObject:withKeyPath:options:” (nedströms) men också i motsatt riktning (uppströms).
Det är uppenbart att gänget runt Cocotron redan funnit ut vad Apples egen NSBinder gör, men Cocotron-implementationen är dock inte en exakt spegel av Apples varianter. Däremot är funktionen för det informella protokollet BindingSupport
den samma. Det är ju ett publicerat API, som då följdaktligen måste implementeras till punkt och pricka om man ska göra en klon av Cocoa.
Jag drog sonika upp F-scripts Klassbrowser , och där letade jag efter klasser med binder
i sig. Mycket riktigt:
I denna klassbrowser ser man att NSBinder
ärver direkt från NSObject
och vidare att ett helt gäng klasser i sin tur ärver från NSBinder
.
Browern visar vilka klassmetoder (meta) methods som finns till varje klass, men den listar inte de vanliga metoderna som agerar på objekt (av den klassen), tyvärr. Jag kan dock inte F-script tillräckligt för att bedöma om jag enkelt kan lista alla metoder.
Används dessa hemliga klasser?
Ja, om man googlar med sökorden “NSBinder” och “cocoa” finner man en massa stackdumpar. Men är det någon (utanför Apple) som direkt, med egen vilja använder dem i sina egna kontrollklasser… Om jag bara visste!
Analys av Cocotrons NSTableView implementation
En fortsättning på min tidigare förvirrade Samlingsvyartikel . Målet är att begripa hur en hemgjord samlingsvy (alltså en vy som presenterar och tillåter editering av många likartade objekt) kan göras. Typexemplet är NSTableView.
Har funnit god inspiration hos Cocotron . Där i källkoden hittar man alla (?) AppKit-klasser implementerade av Christopher Lloyds mfl.
Så här följer djupanalys av vad NSTableView gör i Cocotrons tappning. Notera att detta kräver en god Cocoa-kunskap för att följa.
Ola Bäckström 25 juni 2008
Bakgrund
En NSTableView ärver från NSControl. Det är alltså en NSView som kan hantera input från användaren (och naturligtvis presentera output). De flesta standard-kontroller lämnar över delar av ansvaret till objekt av typen NSActionCell. Dessa celler kan presentera information (alltså “rita” via drawWithFrame:inView:
) men också lagra information (se NCCell
s metoder setObjectValue:
, setStringValue:
, setDoubleValue:
etc).
Men hur snacket mellan kontrollen och actioncellen ser ut är inte särskilt tydligt beskrivet i Apples enda (?) sida om detta . Det som står där gäller inte helt riktigt för NSTableView heller, utan passar bättre på enklare klasser som NSButtonView.
Så om du vill veta mer … häng med!
Open source alternativet Cocotron
Cocotronprojektets uttalade mål är:
…is to provide an easy to use cross-platform solution for Objective-C development. In particular, source code level compatibility with … Apple’s frameworks…
Så trots att det är klasser avsedda att köras på icke-OS X-maskiner (Windows eller Linux) så är det intressant att läsa källkoden. Apples egen hemliga AppKit-källkod kan inte avvika väldigt mycket, eftersom klassernas uppförsel är tänkt vara de samma. Kan nämna att källkoden ser ut att vara i bra skick och att det måste ha varit ett hästjobb att i princip skriva om hela Cocoa från början.
NSTableView, intressanta delar i implementationen
Det jag speciellt vill veta är detta:
- Ansvarsfördelning NSControl mot NSActionCell när det gäller hantering av användarinput.
- På vilket sätt bindings kommer in i bilden. Är NSActionCell involverat över huvud taget?
Det som vi inte får fram i denna dissektion är hur IB-paletter fungerar och inte heller någonting om NSEditor. Det får bli en senare fråga.
Bindings kommer vi bara delvis in på, men jag kan svara redan nu att NSActionCell har inget direkt med bindings att göra. Däremot håller objekt av klassen NSTableColumn
reda på bindingarna, medan själva NSTableView
ser till att dataflödet uppströms fungerar, mha KVC.
Till saken, nu spanar vi in i Cocotrons implementation av NSTableView.m :
Snabböversikt
Den stora majoriteten av metoderna är setters/getters för alla de olika attributen (eller properties, på obj-c-språk), exempel är rowHeight
och intercellSpacing
. Inget konstigt där.
Sedan finns några enkla hjälpfunktioner för att lokalisera rader/kolumner/celler i vyn. Exempel är
rectOfRow:
rectOfColumn:
rowAtPoint:
frameOfCellAtColumn:
rowsInRect:
de gör det man skulle tro de gör. Bra med tydliga metodnamn!
Det finns ett antal getters/setters för datakälla (dataSource) och delegat, verkar ganska rättframt, på några ställen kollas om datakällan verkligen svarar på tilltal. Noteras är att klassen inte kräver att man bara väljer en metod att förse den med tabellinformation: det är fullt möjligt att både ange datasource och via bindings knyta den till en NSArrayController. Det verkar dock som databinding har prioritet över datakälla.
Scenario: Användare klickar på något inne i vyn.
Eftersom vi ärver in från NSResponder
(indirekt via NSControl
) så kommer ett musklick att generera ett anrop till mouseDown:
. Implementationen här är intressant; jag listar en pseudo-kod baserat på den riktiga källkoden:
-(void)mouseDown:(NSEvent *)event {
Finn aktuell rad och kolumn som ligger muspekaren befinner sig på.
Finn vilken cell, låt oss kalla den clickedCell, som ligger på denna rad och kolumn.
OM ( clickedCell ärver från NSButtonCell )
Se till att clickedCell har rätt värde enl. ev. datakälla.
Tala om för clickedCell att den blev tryckt på.
Informera dataKällan (dataSource) om nytt cell-värde. // men binding då? går det inte knyta en knapp i Cocotron?
Hissa NeedDisply-flaggan för vyn.
ANNARS
OM ( det är ett enkelklick )
Långt tjorvigt block som reder ut om det påverkar markeringen (bla beroende på ALT- eller SHIFT-knappar).
Långt tjorvigt block som reder ut om det är en början till drag&drop.
Skicka target-action.
OM ( det är ett dubbelklick )
OM ( datakällan eller ev. binding tillåter editering)
Påbörja celleditering genom att anropa egna metoden editColumn:row:withEvent:select:
ANNARS
Skicka target-doubleAction.
}
Koden här är i verkligheten än mer grisig, men den ska alltså separera på ett antal olika operationer, knapptryckning, markering och drag&drop samt slutligen editering av cellinnehåll. Notera att i fallen drag&drop och editering så slutförs inte operationen här: NSTableView håller reda på att en av dessa operationer har påbörjats.
Jag misstänker att det saknas lite kod runt knapptryckning som har med databinding att göra: det verkar inte vara möjligt att knyta upp en knapp.
Anta att användaren dubbelklickat, för att editera en cell, då blir innehållet i editColumn:row:withEvent:select:
en rackarns viktig funktion. Denna anropas internt från 2 andra platser, nämligen
mouseDown:
, som vi tittat på ovan. Det gällertextDidEndEditing:
. Aha, Eureka, här kommer Field editor in i bilden. Den här metoden anropas av fälteditorn när en celleditering är färdig. Lite beroende på konfiguration så kan en ny celleditering påbörjas. Trycker användaren på TAB så ska cellen till höger påbörjas att editeras.
Men hur kan fälteditorn anropa något i ett NSTableView-objekt? Jo, vid varje texteditering så riggas en delegat till fälteditorn -och i det här fallet sätts då själva NSTableView-objektet. Notera att det inte är den editerade cellen som är delegat!
Scenario: Editering av textfält påbörjas
Nu behöver vi grotta i editColumn:row:withEvent:select:
-(void)editColumn:(int)column row:(int)row withEvent:(NSEvent *)event select:(BOOL)select {
Ett litet stycke som verifierar att: 1. kolumn tillåter editering 2. delegat tillåter editering 3. datakälla (dataSource) tillåter editering ELLER databinding är read-write
// nedan börjar allvaret... Kopiera (via NSCopying) den aktuella cellen, kalla denna nya cell för editingCell OM ( editingCell ärver från NSTextFieldCell ) Justera lite attribut på editingCell, för att se till att den ritar exakt över den ursprungliga cellen. Se till att editingCell har rätt värde enl. ev. datakälla. Se till att editingCell har rätt värde enl. ev. databinding. Begär att få tag på fönstrets fält-editor. Be editingCell att konfigurera fält-editorn. // via NSCells metod setUpFieldEditorAttributes: OM (select-flaggan hissad) anropa editingCells selectWithFrame:inView:editor:delegate:start:length: ANNARS anropa editingCells editWithFrame:inView:editor:delegate:event:
Hissa NeedDisply-flaggan för vyn. }
Här tror jag Cocotron har genat lite, de tillåter alltså bara texteditering om tabellen består av NSTextFieldCell. Men i AppKit kan man stoppa in egna celler.
aha…tag ett djupt andetag nu… som du ser anropas editWithFrame:...
eller selectWithFrame:...
på cellen, dessa anropar i sin tur en privat/hemlig metod _setupFieldEditorWithFrame:controlView:editor:delegate:
ärvt från NSCell. Det är den metoden som gör de viktiga anropen för att “sätta i gång” fälteditorn:
[view addSubview:editor];
[[view window] makeFirstResponder:editor];
[editor setDelegate:delegate];
notera att mer än ovanstående görs; saker som har med scrollbarhet (NSClipView) att göra.
OJOJ. Jag tror jag måste göra en figur baserat på detta. Kanske ett lämpligt ämne för en kommande artikel?