Skip to content

Commit 015b4c9

Browse files
authored
Higher level subclassing API (#207)
* objc: add high level NewClass function for subclassing. update subclass example to showcase api. CHANGE method Class() on struct Class and interface IClass to MetaClass() * objc: add tests for NewClass
1 parent 83f5570 commit 015b4c9

File tree

6 files changed

+192
-32
lines changed

6 files changed

+192
-32
lines changed

macos/_examples/subclass/main.go

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,44 +8,44 @@ import (
88
"github.com/progrium/macdriver/objc"
99
)
1010

11-
type TestView struct {
12-
objc.Object
11+
type CustomView struct {
12+
appkit.View `objc:"NSView"`
1313
}
1414

15-
func (v TestView) acceptsFirstResponder() bool {
15+
func (v CustomView) AcceptsFirstResponder() bool {
1616
return true
1717
}
1818

19-
func (v TestView) keyDown(event appkit.Event) {
19+
func (v CustomView) KeyDown(event appkit.Event) {
2020
log.Println("Keydown:", v.Class().Name(), event.Class().Name())
2121
}
2222

23-
func (v TestView) dividerThickness() float64 {
23+
type CustomSplitView struct {
24+
appkit.SplitView `objc:"NSSplitView"`
25+
}
26+
27+
func (v CustomSplitView) DividerThickness() float64 {
2428
return 10.0
2529
}
2630

27-
func (v TestView) dividerColor() appkit.Color {
31+
func (v CustomSplitView) DividerColor() appkit.Color {
2832
return appkit.Color_BlackColor()
2933
}
3034

3135
func main() {
3236
log.Println("Program started.")
3337

34-
// Create a SplitView subclass using AllocateClass
35-
SplitViewClass := objc.AllocateClass(objc.GetClass("NSSplitView"), "TestSplitView", 0)
36-
objc.AddMethod(SplitViewClass, objc.Sel("acceptsFirstResponder"), (TestView).acceptsFirstResponder)
37-
objc.AddMethod(SplitViewClass, objc.Sel("keyDown:"), (TestView).keyDown)
38-
39-
// Implement these methods for the dividerThickness and dividerColor properties on the subclass
40-
objc.AddMethod(SplitViewClass, objc.Sel("dividerThickness"), (TestView).dividerThickness)
41-
objc.AddMethod(SplitViewClass, objc.Sel("dividerColor"), (TestView).dividerColor)
42-
43-
objc.RegisterClass(SplitViewClass)
38+
CustomViewClass := objc.NewClass[CustomView](
39+
objc.Sel("acceptsFirstResponder"),
40+
objc.Sel("keyDown:"),
41+
)
42+
objc.RegisterClass(CustomViewClass)
4443

45-
ViewClass := objc.AllocateClass(objc.GetClass("NSView"), "TestView", 0)
46-
objc.AddMethod(ViewClass, objc.Sel("acceptsFirstResponder"), (TestView).acceptsFirstResponder)
47-
objc.AddMethod(ViewClass, objc.Sel("keyDown:"), (TestView).keyDown)
48-
objc.RegisterClass(ViewClass)
44+
CustomSplitViewClass := objc.NewClass[CustomSplitView](
45+
objc.Sel("dividerThickness"),
46+
objc.Sel("dividerColor"),
47+
)
48+
objc.RegisterClass(CustomSplitViewClass)
4949

5050
app := appkit.Application_SharedApplication()
5151

@@ -64,11 +64,11 @@ func main() {
6464
win.SetTitle("Hello world")
6565
win.SetLevel(appkit.MainMenuWindowLevel + 2)
6666

67-
view := appkit.SplitViewFrom(SplitViewClass.CreateInstance(0).Ptr()).InitWithFrame(frame)
67+
view := CustomSplitViewClass.New().InitWithFrame(frame)
6868
view.SetVertical(true)
6969

70-
neatView := appkit.ViewFrom(ViewClass.CreateInstance(0).Ptr()).InitWithFrame(rectOf(0, 0, 150, 99))
71-
coolView := appkit.ViewFrom(ViewClass.CreateInstance(0).Ptr()).InitWithFrame(rectOf(10, 0, 150, 99))
70+
neatView := CustomViewClass.New().InitWithFrame(rectOf(0, 0, 150, 99))
71+
coolView := CustomViewClass.New().InitWithFrame(rectOf(10, 0, 150, 99))
7272
neatView.AddSubview(appkit.NewLabel("NEAT"))
7373
coolView.AddSubview(appkit.NewLabel("COOL"))
7474

objc/call.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func directPointer(t reflect.Type) bool {
7979
// AddMethod adds an instance method using a Go function.
8080
// The first argument of the Go function should be the object instance,
8181
// the second argument should be the method selector.
82-
func AddMethod(class Class, sel Selector, f any) bool {
82+
func AddMethod(class IClass, sel Selector, f any) bool {
8383
rf := reflect.ValueOf(f)
8484

8585
typeEncoding := _getMethodTypeEncoding(rf.Type(), false)
@@ -95,7 +95,7 @@ func AddMethod(class Class, sel Selector, f any) bool {
9595
// ReplaceMethod replaces an instance method using a Go function.
9696
// The first argument of the Go function should be the object instance,
9797
// the second argument should be the method selector.
98-
func ReplaceMethod(class Class, sel Selector, f any) {
98+
func ReplaceMethod(class IClass, sel Selector, f any) {
9999
rf := reflect.ValueOf(f)
100100
typeEncoding := _getMethodTypeEncoding(rf.Type(), false)
101101

@@ -110,12 +110,12 @@ func ReplaceMethod(class Class, sel Selector, f any) {
110110
// AddClassMethod adds a class method using a Go function.
111111
// The first argument of the Go function should be the class,
112112
// the second argument should be the method selector.
113-
func AddClassMethod(class Class, sel Selector, f any) bool {
113+
func AddClassMethod(class IClass, sel Selector, f any) bool {
114114
rf := reflect.ValueOf(f)
115115
typeEncoding := _getMethodTypeEncoding(rf.Type(), true)
116116

117117
imp, handle := wrapGoFuncAsMethodIMP(rf)
118-
metaClass := class.Class()
118+
metaClass := class.MetaClass()
119119
if metaClass.Ptr() == nil {
120120
panic("no meta class")
121121
}
@@ -129,7 +129,7 @@ func AddClassMethod(class Class, sel Selector, f any) bool {
129129
// ReplaceClassMethod replaces a class method using a Go function.
130130
// The first argument of the Go function should be the class,
131131
// the second argument should be the method selector.
132-
func ReplaceClassMethod(class Class, sel Selector, f any) {
132+
func ReplaceClassMethod(class IClass, sel Selector, f any) {
133133
rf := reflect.ValueOf(f)
134134
typeEncoding := _getMethodTypeEncoding(rf.Type(), true)
135135

objc/class.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ type IClass interface {
4242
Name() string
4343
SetVersion(version int)
4444
Version() int
45-
Class() Class
45+
MetaClass() Class
4646
SuperClass() Class
4747
RespondsToSelector(sel Selector) bool
4848
AddMethod(sel Selector, imp IMP, types string) bool
@@ -90,14 +90,14 @@ func AllocateClass(superClass Class, name string, extraBytes uint) Class {
9090
// Registers a class that was allocated using [AllocateClass] [Full Topic]
9191
//
9292
// [Full Topic]: https://developer.apple.com/documentation/objectivec/1418603-objc_registerclasspair?language=objc
93-
func RegisterClass(class Class) {
93+
func RegisterClass(class IClass) {
9494
C.Objc_RegisterClassPair(class.Ptr())
9595
}
9696

9797
// Destroys a class and its associated metaclass. [Full Topic]
9898
//
9999
// [Full Topic]: https://developer.apple.com/documentation/objectivec/1418912-objc_disposeclasspair?language=objc
100-
func DisposeClass(class Class) {
100+
func DisposeClass(class IClass) {
101101
C.Objc_DisposeClassPair(class.Ptr())
102102
}
103103

@@ -112,7 +112,7 @@ func (c Class) Name() string {
112112
return name
113113
}
114114

115-
func (c Class) Class() Class {
115+
func (c Class) MetaClass() Class {
116116
return ObjectFrom(c.ptr).Class()
117117
}
118118

objc/object.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ package objc
2828
// const char* Object_Description(void* ptr);
2929
import "C"
3030
import (
31+
"reflect"
3132
"unsafe"
3233
)
3334

@@ -70,6 +71,38 @@ func Ptr(o Handle) unsafe.Pointer {
7071
return o.Ptr()
7172
}
7273

74+
func setPtr(obj any, ptr unsafe.Pointer) {
75+
if o, ok := obj.(*Object); ok {
76+
o.ptr = ptr
77+
} else {
78+
if objPtr := findObjectPointer(reflect.ValueOf(obj)); objPtr != nil {
79+
objPtr.ptr = ptr
80+
} else {
81+
panic("unable to find embedded object")
82+
}
83+
}
84+
}
85+
86+
func findObjectPointer(v reflect.Value) *Object {
87+
if v.Kind() == reflect.Ptr {
88+
v = v.Elem()
89+
}
90+
t := v.Type()
91+
for i := 0; i < v.NumField(); i++ {
92+
if t.Field(i).Type == reflect.TypeOf(Object{}) {
93+
ptr := v.Field(i).Addr().Interface().(*Object)
94+
return ptr
95+
}
96+
if t.Field(i).Anonymous {
97+
f := findObjectPointer(v.Field(i))
98+
if f != nil {
99+
return f
100+
}
101+
}
102+
}
103+
return nil
104+
}
105+
73106
// The root class of most Objective-C class hierarchies, from which subclasses inherit a basic interface to the runtime system and the ability to behave as Objective-C objects. [Full Topic]
74107
//
75108
// [Full Topic]: https://developer.apple.com/documentation/objectivec/nsobject?language=objc

objc/userclass.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package objc
2+
3+
import "reflect"
4+
5+
// UserClass is a generic wrapper around Class returned by NewClass.
6+
type UserClass[T IObject] struct {
7+
Class
8+
}
9+
10+
// New creates an instance of the class then calls init and autorelease before returning.
11+
func (c UserClass[T]) New() T {
12+
var o T
13+
oo := c.CreateInstance(0).PerformSelector(Sel("init"))
14+
setPtr(&o, oo.Ptr())
15+
o.Autorelease()
16+
return o
17+
}
18+
19+
// NewClass will allocate a new class using the name of the type passed
20+
// and NSObject as the superclass unless the passed type has a struct tag
21+
// with the key "objc" specifying the name of the superclass to use.
22+
// The returned class will still need to be registered.
23+
func NewClass[T IObject](selectors ...Selector) UserClass[T] {
24+
var o T
25+
typ := reflect.TypeOf(o)
26+
var super string
27+
for i := 0; i < typ.NumField(); i++ {
28+
super = typ.Field(i).Tag.Get("objc")
29+
if super != "" {
30+
break
31+
}
32+
}
33+
if super == "" {
34+
super = "NSObject"
35+
}
36+
cls := AllocateClass(GetClass(super), typ.Name(), 0)
37+
for _, sel := range selectors {
38+
m, ok := typ.MethodByName(selectorToGoName(sel.Name()))
39+
if !ok {
40+
panic("allocating class from struct without method for selector: " + sel.Name())
41+
}
42+
AddMethod(cls, sel, m.Func.Interface())
43+
}
44+
return UserClass[T]{
45+
Class: cls,
46+
}
47+
}

objc/userclass_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package objc
2+
3+
import (
4+
"testing"
5+
6+
"github.com/progrium/macdriver/internal/assert"
7+
)
8+
9+
// uses NSObject as super without struct tag, regardless of embedded type.
10+
type FooObject struct {
11+
Object
12+
}
13+
14+
func (f FooObject) MethodForFoo() string {
15+
return "foo"
16+
}
17+
18+
// uses FooObject from struct tag as super, which should be registered first.
19+
type BarObject struct {
20+
FooObject `objc:"FooObject"`
21+
}
22+
23+
func (b BarObject) MethodForBar() string {
24+
return "bar"
25+
}
26+
27+
func TestNewClass(t *testing.T) {
28+
FooObjectClass := NewClass[FooObject](
29+
Sel("methodForFoo"),
30+
)
31+
RegisterClass(FooObjectClass)
32+
33+
BarObjectClass := NewClass[BarObject](
34+
Sel("methodForBar"),
35+
)
36+
RegisterClass(BarObjectClass)
37+
38+
// as per our rules, New returned objects are autoreleased
39+
WithAutoreleasePool(func() {
40+
foo := FooObjectClass.New()
41+
// foo go method
42+
if foo.MethodForFoo() != "foo" {
43+
t.Fatal("unexpected return")
44+
}
45+
// foo objc method
46+
fooRet := foo.PerformSelector(Sel("methodForFoo"))
47+
if ToGoString(fooRet.Ptr()) != "foo" {
48+
t.Fatal("unexpected return")
49+
}
50+
51+
bar := BarObjectClass.New()
52+
// bar go method
53+
if bar.MethodForBar() != "bar" {
54+
t.Fatal("unexpected return")
55+
}
56+
// bar objc method
57+
barRet := bar.PerformSelector(Sel("methodForBar"))
58+
if ToGoString(barRet.Ptr()) != "bar" {
59+
t.Fatal("unexpected return")
60+
}
61+
// as well as foo go method
62+
if bar.MethodForFoo() != "foo" {
63+
t.Fatal("unexpected return")
64+
}
65+
// and foo objc method
66+
barRet = bar.PerformSelector(Sel("methodForFoo"))
67+
if ToGoString(barRet.Ptr()) != "foo" {
68+
t.Fatal("unexpected return")
69+
}
70+
})
71+
72+
}
73+
74+
func TestNewClassNoMethod(t *testing.T) {
75+
assert.Panics(t, func() {
76+
NewClass[FooObject](
77+
Sel("noMethodLikeThisOnFooObject"),
78+
)
79+
})
80+
}

0 commit comments

Comments
 (0)