Skip to content

[objective_c] Collections support for NS[Mutable]Dictionary and NS[Mutable]Set #2234

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pkgs/objective_c/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

- Use ffigen 18.2.0
- `NSArray` is now a Dart `Iterable` and `NSMutableArray` is now a Dart `List`.
- `NSDictionary` and `NSMutableDictionary` are now Dart `Map`s.
- `NSSet` and `NSMutableSet` are now Dart `Set`s.
- Add `.toNSNumber()` extension method to `int`, `double`, and `num`.

## 7.1.0
Expand Down
173 changes: 166 additions & 7 deletions pkgs/objective_c/lib/src/objective_c_bindings_generated.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2022,7 +2022,39 @@ class NSDate extends NSObject implements NSCopying, NSSecureCoding {

/// NSDictionary
class NSDictionary extends NSObject
with MapBase<NSCopying, objc.ObjCObjectBase>
implements NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration {
/// Creates a [NSDictionary] from [other].
static NSDictionary of(Map<NSCopying, objc.ObjCObjectBase> other) =>
NSMutableDictionary.of(other);

@override
int get length => count;

@override
objc.ObjCObjectBase? operator [](Object? key) =>
key is NSCopying ? objectForKey_(key) : null;

@override
Iterable<NSCopying> get keys => _NSDictionaryKeyIterable(this);

@override
Iterable<objc.ObjCObjectBase> get values => _NSDictionaryValueIterable(this);

@override
bool containsKey(Object? key) => this[key] != null;

@override
void operator []=(NSCopying key, objc.ObjCObjectBase value) =>
throw UnsupportedError("Cannot modify NSDictionary");

@override
void clear() => throw UnsupportedError("Cannot modify NSDictionary");

@override
objc.ObjCObjectBase? remove(Object? key) =>
throw UnsupportedError("Cannot modify NSDictionary");

NSDictionary._(ffi.Pointer<objc.ObjCObject> pointer,
{bool retain = false, bool release = false})
: super.castFromPointer(pointer, retain: retain, release: release);
Expand Down Expand Up @@ -2228,7 +2260,20 @@ enum NSEnumerationOptions {
}

/// NSEnumerator
class NSEnumerator extends NSObject implements NSFastEnumeration {
class NSEnumerator extends NSObject
implements NSFastEnumeration, Iterator<objc.ObjCObjectBase> {
objc.ObjCObjectBase? _current;

@override
objc.ObjCObjectBase get current => _current!;

@override
@pragma('vm:prefer-inline')
bool moveNext() {
_current = nextObject();
return _current != null;
}

NSEnumerator._(ffi.Pointer<objc.ObjCObject> pointer,
{bool retain = false, bool release = false})
: super.castFromPointer(pointer, retain: retain, release: release);
Expand Down Expand Up @@ -4694,7 +4739,7 @@ class NSMethodSignature extends NSObject {
}

/// NSMutableArray
class NSMutableArray extends NSArray with ListMixin<objc.ObjCObjectBase> {
class NSMutableArray extends NSArray with ListBase<objc.ObjCObjectBase> {
/// Creates a [NSMutableArray] of the given length with [fill] at each
/// position.
///
Expand All @@ -4716,18 +4761,19 @@ class NSMutableArray extends NSArray with ListMixin<objc.ObjCObjectBase> {
for (; len > newLength; --len) removeLastObject();
}

@override
Iterator<objc.ObjCObjectBase> get iterator => _NSArrayIterator(this);

@override
objc.ObjCObjectBase operator [](int index) => objectAtIndex_(index);

@override
void operator []=(int index, objc.ObjCObjectBase value) =>
replaceObjectAtIndex_withObject_(index, value);

@override
void add(objc.ObjCObjectBase value) => addObject_(value);

@override
void addAll(Iterable<objc.ObjCObjectBase> iterable) {
for (final value in iterable) add(value);
}

NSMutableArray._(ffi.Pointer<objc.ObjCObject> pointer,
{bool retain = false, bool release = false})
: super.castFromPointer(pointer, retain: retain, release: release);
Expand Down Expand Up @@ -5339,6 +5385,25 @@ class NSMutableData extends NSData {

/// NSMutableDictionary
class NSMutableDictionary extends NSDictionary {
/// Creates a [NSMutableDictionary] from [other].
static NSDictionary of(Map<NSCopying, objc.ObjCObjectBase> other) =>
NSMutableDictionary.dictionaryWithCapacity_(other.length)..addAll(other);

@override
void clear() => removeAllObjects();

@override
objc.ObjCObjectBase? remove(Object? key) {
if (key is! NSCopying) return null;
final old = this[key];
removeObjectForKey_(key);
return old;
}

@override
void operator []=(NSCopying key, objc.ObjCObjectBase value) =>
setObject_forKey_(value, NSCopying.castFrom(key));

NSMutableDictionary._(ffi.Pointer<objc.ObjCObject> pointer,
{bool retain = false, bool release = false})
: super.castFromPointer(pointer, retain: retain, release: release);
Expand Down Expand Up @@ -6083,6 +6148,28 @@ class NSMutableOrderedSet extends NSOrderedSet {

/// NSMutableSet
class NSMutableSet extends NSSet {
/// Creates a [NSMutableSet] from [elements].
static NSMutableSet of(Iterable<objc.ObjCObjectBase> elements) =>
setWithCapacity_(elements.length)..addAll(elements);

@override
bool add(objc.ObjCObjectBase value) {
final alreadyContains = contains(value);
addObject_(value);
return !alreadyContains;
}

@override
bool remove(Object? value) {
if (value is! objc.ObjCObjectBase) return false;
final alreadyContains = contains(value);
removeObject_(value);
return alreadyContains;
}

@override
void clear() => removeAllObjects();

NSMutableSet._(ffi.Pointer<objc.ObjCObject> pointer,
{bool retain = false, bool release = false})
: super.castFromPointer(pointer, retain: retain, release: release);
Expand Down Expand Up @@ -9379,7 +9466,39 @@ interface class NSSecureCoding extends objc.ObjCProtocolBase

/// NSSet
class NSSet extends NSObject
with SetBase<objc.ObjCObjectBase>
implements NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration {
/// Creates a [NSSet] from [elements].
static NSSet of(Iterable<objc.ObjCObjectBase> elements) =>
NSMutableSet.of(elements);

@override
int get length => count;

@override
bool contains(Object? element) =>
element is objc.ObjCObjectBase ? containsObject_(element) : false;

@override
objc.ObjCObjectBase? lookup(Object? element) =>
element is objc.ObjCObjectBase ? member_(element) : null;

@override
Iterator<objc.ObjCObjectBase> get iterator => objectEnumerator();

@override
Set<objc.ObjCObjectBase> toSet() => {...this};

@override
bool add(objc.ObjCObjectBase value) =>
throw UnsupportedError("Cannot modify NSSet");

@override
bool remove(Object? value) => throw UnsupportedError("Cannot modify NSSet");

@override
void clear() => throw UnsupportedError("Cannot modify NSSet");

NSSet._(ffi.Pointer<objc.ObjCObject> pointer,
{bool retain = false, bool release = false})
: super.castFromPointer(pointer, retain: retain, release: release);
Expand Down Expand Up @@ -18498,3 +18617,43 @@ class _NSArrayIterator implements Iterator<objc.ObjCObjectBase> {
return true;
}
}

class _NSDictionaryKeyIterable with Iterable<NSCopying> {
NSDictionary _dictionary;

_NSDictionaryKeyIterable(this._dictionary);

@override
int get length => _dictionary.length;

@override
Iterator<NSCopying> get iterator =>
_NSDictionaryKeyIterator(_dictionary.keyEnumerator());

@override
bool contains(Object? key) => _dictionary.containsKey(key);
}

class _NSDictionaryValueIterable with Iterable<objc.ObjCObjectBase> {
NSDictionary _dictionary;

_NSDictionaryValueIterable(this._dictionary);

@override
int get length => _dictionary.length;

@override
Iterator<objc.ObjCObjectBase> get iterator => _dictionary.objectEnumerator();
}

class _NSDictionaryKeyIterator implements Iterator<NSCopying> {
final Iterator<objc.ObjCObjectBase> _iterator;
_NSDictionaryKeyIterator(this._iterator);

@override
NSCopying get current => NSCopying.castFrom(_iterator.current);

@override
@pragma('vm:prefer-inline')
bool moveNext() => _iterator.moveNext();
}
113 changes: 113 additions & 0 deletions pkgs/objective_c/test/nsdictionary_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// Objective C support is only available on mac.
@TestOn('mac-os')
library;

import 'dart:ffi';

import 'package:objective_c/objective_c.dart';
import 'package:test/test.dart';

void main() {
group('NSDictionary', () {
setUpAll(() {
// TODO(https://github.com/dart-lang/native/issues/1068): Remove this.
DynamicLibrary.open('test/objective_c.dylib');
});

test('of', () {
final obj1 = 'obj1'.toNSString();
final obj2 = 'obj2'.toNSString();
final obj3 = 'obj3'.toNSString();
final obj4 = 'obj4'.toNSString();
final obj5 = 'obj5'.toNSString();
final obj6 = 'obj6'.toNSString();

final dict = NSDictionary.of({
obj1: obj2,
obj3: obj4,
obj5: obj6,
});

expect(dict.length, 3);
expect(dict[obj1], obj2);
expect(dict[obj3], obj4);
expect(dict[obj5], obj6);

// Keys are copied, so compare the string values.
final actualKeys = <String>[];
for (final key in dict.keys) {
actualKeys.add(NSString.castFrom(key).toDartString());
}
expect(actualKeys, unorderedEquals(['obj1', 'obj3', 'obj5']));

// Values are stored by reference, so we can compare the actual objects.
final actualValues = <ObjCObjectBase>[];
for (final value in dict.values) {
actualValues.add(value);
}
expect(actualValues, unorderedEquals([obj2, obj4, obj6]));
});

test('immutable', () {
final obj1 = 'obj1'.toNSString();
final obj2 = 'obj2'.toNSString();
final obj3 = 'obj3'.toNSString();
final obj4 = 'obj4'.toNSString();
final obj5 = 'obj5'.toNSString();
final obj6 = 'obj6'.toNSString();

// NSDictionary.of actually returns a NSMutableDictionary, so our
// immutability tests wouldn't actually work. So convert it to a real
// NSDictionary using an ObjC constructor.
final dict = NSDictionary.dictionaryWithDictionary_(NSDictionary.of({
obj1: obj2,
obj3: obj4,
obj5: obj6,
}));

expect(() => dict[obj3] = obj1, throwsUnsupportedError);
expect(dict.clear, throwsUnsupportedError);
expect(() => dict.remove(obj1), throwsUnsupportedError);
});

test('MapBase mixin', () {
final obj1 = 'obj1'.toNSString();
final obj2 = 'obj2'.toNSString();
final obj3 = 'obj3'.toNSString();
final obj4 = 'obj4'.toNSString();
final obj5 = 'obj5'.toNSString();
final obj6 = 'obj6'.toNSString();

final dict = NSDictionary.of({
obj1: obj2,
obj3: obj4,
obj5: obj6,
});

expect(dict.isNotEmpty, isTrue);
expect(dict.containsKey(obj1), isTrue);
expect(dict.containsKey(obj4), isFalse);
expect(dict.containsValue(obj2), isTrue);
expect(dict.containsValue(obj3), isFalse);

expect(
dict.map((key, value) =>
MapEntry<ObjCObjectBase, ObjCObjectBase>(value, key)),
{
obj2: obj1,
obj4: obj3,
obj6: obj5,
});
expect(
dict.keys
.map((key) => NSString.castFrom(key).toDartString())
.toList(),
unorderedEquals(['obj1', 'obj3', 'obj5']));
expect(dict.values.toList(), unorderedEquals([obj2, obj4, obj6]));
});
});
}
Loading
Loading