Menu

The skip-model framework is available at https://github.com/skiptools/skip-model.git, which can be checked out and tested with skip test once Skip is installed.

SkipModel

Model object observation for Skip apps.

See what API is included here.

About

SkipModel vends the skip.model Kotlin package. This package contains Observable and ObservableObject interfaces, representing the two core protocols that SwiftUI uses to observe changes to model objects. It also includes limited Publisher support.

Dependencies

SkipLib depends on the skip transpiler plugin and the SkipFoundation package.

SkipModel is part of the core SkipStack and is not intended to be imported directly. The transpiler includes import skip.model.* in generated Kotlin for any Swift source that imports the Combine, Observation, or SwiftUI frameworks.

Status

From the Observation package, SkipModel supports the @Observable and @ObservationIgnored macros.

From Combine, SkipModel supports the ObservableObject protocol, the @Published property wrapper, and limited Publisher functionality. See API support below.

Much of Skip’s model support is implemented directly in the Skip transpiler. The Observable and ObservableObject marker protocols are are sufficient for the Skip transpiler to recognize your observable types. When generating their corresponding Kotlin classes, the transpiler then adds the necessary code so that their state can be tracked by the Compose runtime.

Contributing

We welcome contributions to SkipModel. The Skip product documentation includes helpful instructions and tips on local Skip library development. When submitting code, please include unit tests in your PR.

Model Objects

Like Skip itself, SkipModel objects are dual-platform! Not only do your @Observable and ObservableObject properties participate in SwiftUI state tracking, but they are tracked by Compose as well. The Skip transpiler backs your observable properties with MutableState values in Kotlin, so Compose automatically tracks reads and writes and performs recomposition as needed.

This means that you can write shared model-layer Swift code using observable objects, and use it to power both SwiftUI (whether iOS-only or dual-platform with Skip) as well as pure Android Compose UI code. For example, the following model class:

@Observable class TapCounter {
    var tapCount = 0 
}

could power a Compose UI:

val tapCounter = TapCounter()
...
TapIt(counter = tapCounter)
...
@Composable fun TapIt(counter: TapCounter) {
    Button(onClick = { counter.tapCount += 1 }) { 
        Text("Tap Count: ${counter.tapCount}")
    }
}

API Support

The following table summarizes SkipModel’s API support on Android. Anything not listed here is likely not supported. Note that in your iOS-only code - i.e. code within #if !SKIP blocks - you can use any Swift API you want. Additionally:

  • In all Combine publishes and related API, the Failure type must be Never: throwing errors in Combine chains is not supported.
  • In Skip, Combine is not automatically imported when you import Foundation. Make sure to import Combine or import SwiftUI explicitly.

Support levels:

  • βœ… – Full
  • 🟒 – High
  • 🟑 – Medium
  • 🟠 – Low
SupportAPI
🟒
AnyCancellable
  • See Cancellable
🟠
AnyPublisher
  • init(_ publisher: Publisher)
  • See Publisher
🟒
Cancellable
  • The store(in:) function only supports a Set
🟠
ConnectablePublisher
  • func connect()
  • func autoconnect()
  • See Publisher
βœ… func NotificationCenter.publisher(for: Notification.Name, object: Any? = nil): Publisher<Notification, Never>
🟒
@Observable
  • Skip does not support calls to the generated access(keyPath:) and withMutation(keyPath:_:) functions
🟒
ObservableObject
  • If you declare your own objectWillChange publisher, it must be of type ObservableObjectPublisher
🟠
ObservableObjectPublisher
  • func send()
  • See Publisher
βœ… @ObservationIgnored
🟠
PassthroughSubject
  • func send(value: Output)
  • See Publisher
βœ… @Published
🟠
Publisher
  • func assign<Root>(to: KeyPath<Root, Output>, on: Root) -> AnyCancellable
  • func sink(receiveValue: (Output) -> Unit) -> AnyCancellable
  • func debounce(for: Double, scheduler: Scheduler) -> Publisher
  • func dropFirst(count: Int = 1) -> Publisher
  • func filter(isIncluded: (Output) -> Boolean) -> Publisher
  • func map<T>(transform: (Output) -> T) -> Publisher
  • func receive(on: Scheduler): Publisher
  • func eraseToAnyPublisher(): AnyPublisher
βœ… func Timer.publish(every: TimeInterval, tolerance: TimeInterval? = nil, on runLoop: RunLoop, in mode: RunLoop.Mode, options: RunLoop.SchedulerOptions? = nil) -> ConnectablePublisher<Date, Never>