This Skip framework is available at https://source.skip.tools/skip-foundation.git, which can be checked out and tested with skip test
once Skip is installed.
SkipFoundation
Foundation support for Skip apps.
About
SkipFoundation vends the skip.foundation
Kotlin package. It is a reimplementation of Foundation for Kotlin on Android. Its goal is to mirror as much of Foundation as possible, allowing Skip developers to use Foundation API with confidence.
Dependencies
SkipFoundation depends on the skip transpiler plugin as well as the SkipLib package.
SkipFoundation is part of the core SkipStack and is not intended to be imported directly.
The module is transparently adopted through the translation of import Foundation
into import skip.foundation.*
by the Skip transpiler.
Status
SkipFoundation supports many of the Foundation framework’s most common APIs, but there are many more that are not yet ported. The best way to monitor SkipFoundation’s status is through its comprehensive set of Tests. A skipped test generally means the API has not been implemented.
When you want to use a Foundation API that has not been implemented, you have options. You can try to find a workaround using only supported API, embed Kotlin code directly as described in the Skip docs, or add support to SkipFoundation. If you choose to enhance SkipFoundation itself, please consider contributing your code back for inclusion in the official release.
Contributing
We welcome contributions to SkipFoundation. The Skip product documentation includes helpful instructions and tips on local Skip library development.
The most pressing need is to implement more of the most-used Foundation APIs. To help fill in unimplemented API in SkipFoundation:
- Find unimplemented API. Unimplemented API will generally be commented out or have TODO comments in the source. The set of skipped tests also gives a high-level view of what is not yet ported to Skip.
- Write an appropriate Kotlin implementation. See Implementation Strategy below.
- Edit the corresponding tests to make sure they are no longer skipped, and that they pass. If there aren’t existing tests, write some. See Tests.
- Submit a PR.
Other forms of contributions such as test cases, comments, and documentation are also welcome!
Implementation Strategy
The goal of SkipFoundation is to mirror the Foundation framework for Android. iOS developers will never use SkipFoundation directly, because they already have access to Foundation. Nevertheless, SkipFoundation’s API ports include both an Android branch and an iOS branch. SkipFoundation is structured more like a library that is providing new functionality to both platforms than as a Skip library designed to replicate existing iOS functionality on Android.
Why is SkipFoundation structured this way? Two reasons:
- SkipFoundation can serve as an example to developers who want to create new dual-platform Skip libraries. The patterns used here are the same patterns you employ to vend a library that Skip developers can use within their iOS code, but will also function in their transpiled Android apps.
- Although the iOS code branches will never be used in an iOS app, they are exercised by SkipFoundation’s comprehensive tests. The experience of writing the iOS implementation first and verifying that it passes the test suite helps us design and validate the Android implementation.
SkipFoundation uses #if SKIP
compiler directives extensively to inline the use of Kotlin and Java API. See the Skip documentation for more information on Android customization.
Example
Many Foundation types have very close analogs from Kotlin or Java. A SkipFoundation implementation, therefore, often looks like something like Calendar
:
#if !SKIP
@_implementationOnly import struct Foundation.Calendar
internal typealias PlatformCalendar = Foundation.Calendar
#else
public typealias PlatformCalendar = java.util.Calendar
#endif
public struct Calendar : Hashable, CustomStringConvertible {
internal var platformValue: PlatformCalendar
#if SKIP
public var locale: Locale
#endif
public static var current: Calendar {
#if !SKIP
return Calendar(platformValue: PlatformCalendar.current)
#else
return Calendar(platformValue: PlatformCalendar.getInstance())
#endif
}
internal init(platformValue: PlatformCalendar) {
self.platformValue = platformValue
#if SKIP
self.locale = Locale.current
#endif
}
...
public var amSymbol: String {
#if !SKIP
return platformValue.amSymbol
#else
return dateFormatSymbols.getAmPmStrings()[0]
#endif
}
...
}
When a Foundation type wraps a corresponding Kotlin or Java type, please provide Skip’s standard .kotlin()
and .swift()
functions for converting between the two:
#if SKIP
extension Calendar {
public func kotlin(nocopy: Bool = false) -> java.util.Calendar {
return nocopy ? platformValue : platformValue.clone() as java.util.Calendar
}
}
extension java.util.Calendar {
public func swift(nocopy: Bool = false) -> Calendar {
let platformValue = nocopy ? self : clone() as java.util.Calendar
return Calendar(platformValue: platformValue)
}
}
#endif
Topics
Files
Skip implements much of Foundation.FileManager
, which should be
the primary interface for interacting with the file system.
The app-specific folder can be accessed like:
// on Android, this is Context.getFilesDir()
let folder = try FileManager.default.url(for: FileManager.SearchPathDirectory.documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask, appropriateFor: nil, create: false)
And to read and write to the cache folders:
// on Android, this is Context.getCachesDir()
let caches = try FileManager.default.url(for: FileManager.SearchPathDirectory.cachesDirectory, in: FileManager.SearchPathDomainMask.userDomainMask, appropriateFor: nil, create: false)
And the system temporary folder can be accessed with:
let tmpdir = NSTemporaryDirectory()
None of the other FileManager.SearchPathDirectory
enumerations are implemented in Skip.
Both Data
and String
have the ability to read and write to and from URLs and path strings.
Codable
Swift uses the Encodable
and Decodable
protocols to convert objects to and from various data formats. In keeping with its philosophy of transparent adoption, Skip supports Encodable
, Decodable
, and the combined Codable
protocols for object serialization and deserialization. This includes automatic synthesis of default encoding and decoding as well as support for custom encoding and decoding using Swift’s Encodable
and Decodable
APIs. Skip does, however, have some restrictions:
- JSON is currently the only supported format. SkipFoundation includes Foundation’s
JSONEncoder
andJSONDecoder
classes. - Not all JSON formatting options are supported.
Array
,Set
, andDictionary
are supported, but nesting of these types is limited to arrays-of-arrays and dictionaries-of-array-values. Skip does not yet support e.g. an array of dictionaries, or a dictionary with array keys.-
When implementing your own
init(from: Decoder)
decoding, yourdecode
calls must supply a concrete type literal to decode. The following will work:init(from decoder: Decoder) throws { var container = try decoder.container(keyedBy: CodingKeys.self) self.array = try container.decode([Int].self, forKey: CodingKeys.array) }
But these examples will not work:
init(from decoder: Decoder) throws { var container = try decoder.container(keyedBy: CodingKeys.self) let arrayType = [Int].self self.array = try container.decode(arrayType, forKey: CodingKeys.array) } init(from decoder: Decoder) throws { var container = try decoder.container(keyedBy: CodingKeys.self) // T is a generic type of this class self.array = try container.decode([T].self, forKey: CodingKeys.array) }
- As in the examples above, you must fully qualify your
CodingKeys
cases when callingdecode(_:forKey:)
.
Tests
SkipFoundation’s Tests/
folder contains the entire set of official Foundation framework test cases. Through the magic of SkipUnit, this allows us to validate our SkipFoundation API implementations on Android against the same test suite used by the Foundation team on iOS.
It is SkipFoundation’s goal to include - and pass - as much of the official test suite as possible.